數據描述:
半徑,周長,面積均用實型數表示
數據處理:
輸入半徑 r;
計算周長 = 2*π*r ;
計算面積 = π* r2 ;
輸出半徑,周長,面積;
方法1:用結構化方法編程,求圓的周長和面積
// count the girth and area of circle
#include
using name std;
void main ()
{ double r, girth, area ;
const double PI = 3.1415 ;
cout << "Please input radius:\n" ; //操作符重載
cin >> r ; //輸入
girth = 2 * PI * r ;
area = PI * r * r ;
cout << "radius = " << r << endl ;
cout << "girth = " << girth << endl ;
cout << "area = " << area << endl ;
}
方法2:用面向對象方法編程,求圓的周長和面積
#include
using name std;
class Circle
{ double radius ; //成員變量
public : //類的訪問控制
void Set_Radius( double r ) { radius = r ; } //成員函數
double Get_Radius() { return radius ; } //通過成員函數設置成員變量
double Get_Girth() { return 2 * 3.14f * radius ; } //通過成員函數獲取成員變量
double Get_Area() { return 3.14f * radius * radius ; }
} ;
void main()
{
Circle A, B ; //用類定義對象
A.Set_Radius( 6.23 ) ; //類的調用
cout << "A.Radius = " << A.Get_Radius() << endl ;
cout << "A.Girth = " << A.Get_Girth() << endl ;
cout << "A.Area = " << A.Get_Area() << endl ;
B.Set_Radius( 10.5 ) ;
cout << "B.radius = " << B.Get_Radius() << endl ;
cout << "B.Girth=" << B.Get_Girth() << endl ;
cout << "B.Area = " << B.Get_Area() << endl ;
}
總結:建立類、對象、成員變量、成員函數,輸入輸入流基本概念。
// demo02_circle_err.cpp
#include
using namespace std;//c++的命名空間
class circle
{
public:
double r;
double pi = 3.1415926;
double area = pi*r*r;
};
int main()
{
circle pi;
cout << "請輸入area" << endl;
cin >> pi.r;
cout << pi.area << endl; //亂碼
system("pause");
return 0;
}
總結: 從內存四區的角度,解釋為什麼會出現亂碼
理解為什麼需要成員函數
面向過程的結構化程序設計方法
l 設計思路
– 自頂向下、逐步求精。采用模塊分解與功能抽象,自頂向下、分而治之。
l 程序結構:
– 按功能劃分為若干個基本模塊,形成一個樹狀結構。
– 各模塊間的關系盡可能簡單,功能上相對獨立;每一模塊內部均是由順序、選擇和循環三種基本結構組成。
– 其模塊化實現的具體方法是使用子程序。
l 優點:
有效地將一個較復雜的程序系統設計任務分解成許多易於控制和處理的子任務,便於開發和維護。
l 缺點:可重用性差、數據安全性差、難以開發大型軟件和圖形界面的應用軟件
– 把數據和處理數據的過程分離為相互獨立的實體。
– 當數據結構改變時,所有相關的處理過程都要進行相應的修改。
– 每一種相對於老問題的新方法都要帶來額外的開銷。
– 圖形用戶界面的應用程序,很難用過程來描述和實現,開發和維護也都很困難。
面向對象的方法
l 將數據及對數據的操作方法封裝在一起,作為一個相互依存、不可分離的整體——對象。
l 對同類型對象抽象出其共性,形成類。
l 類通過一個簡單的外部接口,與外界發生關系。
l 對象與對象之間通過消息進行通信。
面向對象的基本概念
對象
l 一般意義上的對象:
– 是現實世界中一個實際存在的事物。
– 可以是有形的(比如一輛汽車),也可以是無形的(比如一項計劃)。
– 是構成世界的一個獨立單位,具有
l 靜態特征:可以用某種數據來描述
l 動態特征:對象所表現的行為或具有的功能
l 面向對象方法中的對象:
– 是系統中用來描述客觀事物的一個實體,它是用來構成系統的一個基本單位。對象由一組屬性和一組行為構成。
– 屬性:用來描述對象靜態特征的數據項。
– 行為:用來描述對象動態特征的操作序列。
類
l 分類——人類通常的思維方法
l 分類所依據的原則——抽象
– 忽略事物的非本質特征,只注意那些與當前目標有關的本質特征,從而找出事物的共性,把具有共同性質的事物劃分為一類,得出一個抽象的概念。
– 例如,石頭、樹木、汽車、房屋等都是人們在長期的生產和生活實踐中抽象出的概念。
l 面向對象方法中的"類"
– 具有相同屬性和服務的一組對象的集合
– 為屬於該類的全部對象提供了抽象的描述,包括屬性和行為兩個主要部分。
– 類與對象的關系:
猶如模具與鑄件之間的關系,一個屬於某類的對象稱為該類的一個實例。
封裝
也就是把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
l 把對象的屬性和服務結合成一個獨立的系統單元。
l 盡可能隱蔽對象的內部細節。對外形成一個邊界(或者說一道屏障),只保留有限的對外接口使之與外部發生聯系。
l 繼承對於軟件復用有著重要意義,是面向對象技術能夠提高軟件開發效率的重要原因之一。
l 定義:特殊類的對象擁有其一般類的全部屬性與服務,稱作特殊類對一般類的繼承。
l 例如:將輪船作為一個一般類,客輪便是一個特殊類。
多態
多態是指在一般類中定義的屬性或行為,被特殊類繼承之後,可以具有不同的數據類型或表現出不同的行為。這使得同一個屬性或行為在一般類及其各個特殊類中具有不同的語義。
面向對象的軟件工程
l 面向對象的軟件工程是面向對象方法在軟件工程領域的全面應用。它包括:
– 面向對象的分析(OOA)
– 面向對象的設計(OOD)
– 面向對象的編程(OOP)
– 面向對象的測試(OOT)
– 面向對象的軟件維護(OOSM)
總結:
面向過程程序設計:數據結構 + 算法
主要解決科學計算問題,用戶需求簡單而固定
特點:
分析解決問題所需要的步驟
利用函數實現各個步驟
依次調用函數解決問題
問題:
軟件可重用性差
軟件可維護性差
構建的軟件無法滿足用戶需求
面向對象程序設計:由現實世界建立軟件模型
將現實世界中的事物直接映射到程序中,可直接滿足用戶需求
特點:
直接分析用戶需求中涉及的各個實體
在代碼中描述現實世界中的實體
在代碼中關聯各個實體協同工作解決問題
優勢:
構建的軟件能夠適應用戶需求的不斷變化
直接利用面向過程方法的優勢而避開其劣勢
C語言是在實踐的過程中逐步完善起來的
沒有深思熟慮的設計過程
使用時存在很多“灰色地帶”
殘留量過多低級語言的特征
直接利用指針進行內存操作
C語言的目標是高效
最終程序執行效率的高效
當面向過程方法論暴露越來越多的缺陷的時候,業界開始考慮在工程項目中引入面向對象的設計方法,而第一個需要解決的問題就是:高效的面向對象語言,並且能夠兼容已經存在的代碼。
C語言 + 面向對象方法論===》Objective C /C++
C語言和C++並不是對立的競爭關系
C++是C語言的加強,是一種更好的C語言
C++是以C語言為基礎的,並且完全兼容C語言的特性
學習C++並不會影響原有的C語言知識,相反會根據加深對C的認知;
學習C++可以接觸到更多的軟件設計方法,並帶來更多的機會。
1) C++是一種更強大的C,通過學習C++能夠掌握更多的軟件設計方法
2) C++是Java/C#/D等現代開發語言的基礎,學習C++後能夠快速掌握這些語言
3)C++是各大知名軟件企業挑選人才的標准之一
所謂namespace,是指標識符的各種可見范圍。C++標准程序庫中的所有標識符都被定義於一個名為std的namespace中。
一 :
1)當使用
2)當使用
二: 由於namespace的概念,使用C++標准程序庫的任何標識符時,可以有三種選擇:
1、直接指定標識符。例如std::ostream而不是ostream。完整語句如下:std::cout << std::hex << 3.4 << std::endl;
2、使用using關鍵字。 usingstd::cout; using std::endl; using std::cin; 以上程序可以寫成 cout<< std::hex << 3.4 << endl;
3、最方便的就是使用usingnamespace std; 例如: using namespace std;這樣命名空間std內定義的所有標識符都有效(曝光)。就好像它們被聲明為全局變量一樣。那麼以上語句可以如下寫: cout <
/*
在C++中,名稱(name)可以是符號常量、變量、宏、函數、結構、枚舉、類和對象等等。為了避免,在大規模程序的設計中,以及在程序員使用各種各樣的C++庫時,這些標識符的命名發生沖突,
標准C++引入了關鍵字namespace(命名空間/名字空間/名稱空間/名域),可以更好地控制標識符的作用域。
*/
/*
std是c++標准命名空間,c++標准程序庫中的所有標識符都被定義在std中,比如標准庫中的類iostream、vector
等都定義在該命名空間中,使用時要加上using聲明(using namespace std) 或using指示(如std::string、
std::vector
*/
/*
C中的命名空間
在C語言中只有一個全局作用域
C語言中所有的全局標識符共享同一個作用域
標識符之間可能發生沖突
C++中提出了命名空間的概念
命名空間將全局作用域分成不同的部分
不同命名空間中的標識符可以同名而不會發生沖突
命名空間可以相互嵌套
全局作用域也叫默認命名空間
*/
/*
C++命名空間的定義:
namespace name { … }
*/
/*
C++命名空間的使用:
使用整個命名空間:using namespace name;
使用命名空間中的變量:using name::variable;
使用默認命名空間中的變量:::variable
默認情況下可以直接使用默 認命名空間中的所有標識符
*/
namespace NameSpaceA
{
int a = 0;
}
namespace NameSpaceB
{
int a = 1;
namespace NameSpaceC
{
struct Teacher
{
char name[10];
int age;
};
}
}
int main()
{
using namespace NameSpaceA;
using NameSpaceB::NameSpaceC::Teacher;
printf("a = %d\n", a);
printf("a = %d\n", NameSpaceB::a);
NameSpaceB::NameSpaceC::Teacher t2
Teacher t1 = {"aaa", 3};
printf("t1.name = %s\n", t1.name);
printf("t1.age = %d\n", t1.age);
system("pause");
return 0;
}
1) 當使用
2)c++標准為了和C區別開,也為了正確使用命名空間,規定頭文件不使用後綴.h。
3)C++命名空間的定義: namespacename { …}
4)using namespace NameSpaceA;
5)namespce定義可嵌套。
#include "iostream"
using namespace std;
//C語言中的變量都必須在作用域開始的位置定義!!
//C++中更強調語言的“實用性”,所有的變量都可以在需要使用時再定義。
int main11()
{
int i = 0;
printf("ddd");
int k;
system("pause");
return 0;
}
//register關鍵字 請求編譯器讓變量a直接放在寄存器裡面,速度快
//在c語言中 register修飾的變量 不能取地址,但是在c++裡面做了內容
/*
//1
register關鍵字的變化
register關鍵字請求“編譯器”將局部變量存儲於寄存器中
C語言中無法取得register變量地址
在C++中依然支持register關鍵字
C++編譯器有自己的優化方式,不使用register也可能做優化
C++中可以取得register變量的地址
//2
C++編譯器發現程序中需要取register變量的地址時,register對變量的聲明變得無效。
//3
早期C語言編譯器不會對代碼進行優化,因此register變量是一個很好的補充。
*/
int main22()
{
register int a = 0;
printf("&a = %x\n", &a);
system("pause");
return 0;
}
其他補充:請閱讀《register關鍵字常識課外閱讀.docx》
/*
在C語言中,重復定義多個同名的全局變量是合法的
在C++中,不允許定義多個同名的全局變量
C語言中多個同名的全局變量最終會被鏈接到全局數據區的同一個地址空間上
int g_var;
int g_var = 1;
C++直接拒絕這種二義性的做法。
*/
int main(int argc, char *argv[])
{
printf("g_var = %d\n", g_var);
return 0;
}
struct類型的加強:
C語言的struct定義了一組變量的集合,C編譯器並不認為這是一種新的類型
C++中的struct是一個新類型的定義聲明
struct Student
{
char name[100];
int age;
};
int main(int argc, char *argv[])
{
Student s1 = {"wang", 1};
Student s2 = {"wang2", 2};
return 0;
}
/*
C++中所有的變量和函數都必須有類型
C語言中的默認類型在C++中是不合法的
函數f的返回值是什麼類型,參數又是什麼類型?
函數g可以接受多少個參數?
*/
//更換成.cpp試試
f(i)
{
printf("i = %d\n", i);
}
g()
{
return 5;
}
int main(int argc, char *argv[])
{
f(10);
printf("g() = %d\n", g(1, 2, 3, 4, 5));
getchar();
return 0;
}
總結:
/*
在C語言中
intf( );表示返回值為int,接受任意參數的函數
intf(void);表示返回值為int的無參函數
在C++中
intf( );和int f(void)具有相同的意義,都表示返回值為int的無參函數
*/
C++更加強調類型,任意的程序元素都必須顯示指明類型
4.2-4.6屬於語法級別的增強。
/*
C++中的布爾類型
C++在C語言的基本類型系統之上增加了bool
C++中的bool可取的值只有true和false
理論上bool只占用一個字節,
如果多個bool變量定義在一起,可能會各占一個bit,這取決於編譯器的實現
true代表真值,編譯器內部用1來表示
false代表非真值,編譯器內部用0來表示
bool類型只有true(非0)和false(0)兩個值
C++編譯器會在賦值時將非0值轉換為true,0值轉換為false
*/
int main(int argc, char *argv[])
{
int a;
bool b = true;
printf("b = %d, sizeof(b) = %d\n", b, sizeof(b));
b = 4;
a = b;
printf("a = %d, b = %d\n", a, b);
b = -4;
a = b;
printf("a = %d, b = %d\n", a, b);
a = 10;
b = a;
printf("a = %d, b = %d\n", a, b);
b = 0;
printf("b = %d\n", b);
system("pause");
return 0;
}
int main()
{
int a = 10;
int b = 20;
//返回一個最小數 並且給最小數賦值成3
//三目運算符是一個表達式 ,表達式不可能做左值
(a < b ? a : b )= 30;
printf("a = %d, b = %d\n", a, b);
system("pause");
return 0;
}
1)C語言返回變量的值 C++語言是返回變量本身
C語言中的三目運算符返回的是變量值,不能作為左值使用
C++中的三目運算符可直接返回變量本身,因此可以出現在程序的任何地方
2)注意:三目運算符可能返回的值中如果有一個是常量值,則不能作為左值使用
(a < b ? 1 :b )= 30;
3)C語言如何支持類似C++的特性呢?
====>當左值的條件:要有內存空間;C++編譯器幫助程序員取了一個地址而已
思考:如何讓C中的三目運算法當左值呢?
int main()
{
const int a;
int const b;
const int *c;
int * const d;
const int * const e ;
return 0;
}
Int func1(const )
初級理解:const是定義常量==》const意味著只讀
含義:
//第一個第二個意思一樣 代表一個常整形數
//第三個 c是一個指向常整形數的指針(所指向的內存數據不能被修改,但是本身可以修改)
//第四個 d 常指針(指針變量不能被修改,但是它所指向內存空間可以被修改)
//第五個 e一個指向常整形的常指針(指針和它所指向的內存空間,均不能被修改)
Const好處
//合理的利用const,
//1指針做函數參數,可以有效的提高代碼可讀性,減少bug;
//2清楚的分清參數的輸入和輸出特性
int setTeacher_err( const Teacher *p)
Const修改形參的時候,在利用形參不能修改指針所向的內存空間
int main()
{
const int a = 10;
int *p = (int*)&a;
printf("a===>%d\n", a);
*p = 11;
printf("a===>%d\n", a);
printf("Hello......\n");
return 0;
}
解釋:
C++編譯器對const常量的處理
當碰見常量聲明時,在符號表中放入常量 =è問題:那有如何解釋取地址
編譯過程中若發現使用常量則直接以符號表中的值替換
編譯過程中若發現對const使用了extern或者&操作符,則給對應的常量分配存儲空間(兼容C)
?聯想: int &a = 1(err) & const int &a = 10(ok)?
C++中const符號表原理圖
注意:
C++編譯器雖然可能為const常量分配空間,但不會使用其存儲空間中的值。
結論:
C語言中的const變量
C語言中const變量是只讀變量,有自己的存儲空間
C++中的const常量
可能分配存儲空間,也可能不分配存儲空間
當const常量為全局,並且需要在其它文件中使用
當使用&操作符取const常量的地址
//練習 解釋為什麼
//#define N 10
int main()
{
const int a = 1;
const int b = 2;
int array[a + b ] = {0};
int i = 0;
for(i=0; i<(a+b); i++)
{
printf("array[%d] = %d\n", i, array[i]);
}
getchar();
return 0;
}
C++中的const修飾的,是一個真正的常量,而不是C中變量(只讀)。在const修飾的常量編譯期間,就已經確定下來了。
對比加深
C++中的const常量類似於宏定義
const int c = 5; ≈ #define c 5
C++中的const常量與宏定義不同
const常量是由編譯器處理的,提供類型檢查和作用域檢查
宏定義由預處理器處理,單純的文本替換
//在func1定義a,在func2中能使用嗎?
//在func1中定義的b,在func2中能使用嗎?
練習
void fun1()
{
#define a 10
const int b = 20;
//#undef a # undef
}
void fun2()
{
printf("a = %d\n", a);
//printf("b = %d\n", b);
}
int main()
{
fun1();
fun2();
return 0;
}
C語言中的const變量
C語言中const變量是只讀變量,有自己的存儲空間
C++中的const常量
可能分配存儲空間,也可能不分配存儲空間
當const常量為全局,並且需要在其它文件中使用,會分配存儲空間
當使用&操作符,取const常量的地址時,會分配存儲空間
當const int &a = 10; const修飾引用時,也會分配存儲空間
變量名回顧
變量名實質上是一段連續存儲空間的別名,是一個標號(門牌號)
程序中通過變量來申請並命名內存空間
通過變量的名字可以使用存儲空間
問題1:對一段連續的內存空間只能取一個別名嗎?
a)在C++中新增加了引用的概念
b)引用可以看作一個已定義變量的別名
c)引用的語法:Type& name = var;
d) 引用做函數參數那?(引用作為函數參數聲明時不進行初始化)
void main01()
{
int a = 10; //c編譯器分配4個字節內存。。。a內存空間的別名
int &b = a; //b就是a的別名。。。
a =11; //直接賦值
{
int *p = &a;
*p = 12;
printf("a %d \n",a);
}
b = 14;
printf("a:%d b:%d", a, b);
system("pause");
}
屬於C++編譯器對C的擴展
問題:C中可以編譯通過嗎?
int main()
{
int a = 0;
int &b = a; //int * const b = &a
b = 11; //*b = 11;
return 0;
}
結論:請不要用C的語法考慮 b=11
普通引用在聲明時必須用其它的變量進行初始化,
引用作為函數參數聲明時不進行初始化
//05復雜數據類型 的引用
struct Teacher
{
char name[64];
int age ;
};
void printfT(Teacher *pT)
{
cout<
}
//pT是t1的別名 ,相當於修改了t1
void printfT2(Teacher &pT)
{
//cout<
pT.age = 33;
}
//pT和t1的是兩個不同的變量
void printfT3(Teacher pT)
{
cout<
pT.age = 45; //只會修改pT變量 ,不會修改t1變量
}
void main()
{
Teacher t1;
t1.age = 35;
printfT(&t1);
printfT2(t1); //pT是t1的別名
printf("t1.age:%d \n", t1.age); //33
printfT3(t1) ;// pT是形參 ,t1 copy一份數據 給pT //---> pT = t1
printf("t1.age:%d \n", t1.age); //35
cout<<"hello..."<
system("pause");
return ;
}
1)引用作為其它變量的別名而存在,因此在一些場合可以代替指針
2)引用相對於指針來說具有更好的可讀性和實用性
思考1:C++編譯器背後做了什麼工作?
int main()
{
int a = 10;
int &b = a;
//b是a的別名,請問c++編譯器後面做了什麼工作?
b = 11;
cout<<"b--->"<
printf("a:%d\n", a);
printf("b:%d\n", b);
printf("&a:%d\n", &a);
printf("&b:%d\n", &b); //請思考:對同一內存空間可以取好幾個名字嗎?
system("pause");
return 0;
}
單獨定義的引用時,必須初始化;說明很像一個常量
思考2:普通引用有自己的空間嗎?
struct Teacer {
int &a;
int &b;
};
int main()
{
printf("sizeof(Teacher) %d\n", sizeof(Teacer));
system("pause");
return 0;
}
引用是一個有地址,引用是常量。。。。。
char *const p
1)引用在C++中的內部實現是一個常指針
Type& name ?èType*const name
2)C++編譯器在編譯過程中使用常指針作為引用的內部實現,因此引用所占用的空間大小與指針相同。
3)從使用的角度,引用會讓人誤會其只是一個別名,沒有自己的存儲空間。這是C++為了實用性而做出的細節隱藏
Int main()
{
int x= 10;
func(x);
}
4) 請仔細對比間接賦值成立的三個條件
1定義兩個變量(一個實參一個形參)
2建立關聯實參取地址傳給形參
3*p形參去間接的修改實參的值
1)引用在實現上,只不過是把:間接賦值成立的三個條件的後兩步和二為一
//當實參傳給形參引用的時候,只不過是c++編譯器幫我們程序員手工取了一個實參地址,傳給了形參引用(常量指針)
2)當我們使用引用語法的時,我們不去關心編譯器引用是怎麼做的
當我們分析奇怪的語法現象的時,我們才去考慮c++編譯器是怎麼做的
C++引用使用時的難點:
當函數返回值為引用時
若返回棧變量
不能成為其它引用的初始值
不能作為左值使用
若返回靜態變量或全局變量
可以成為其他引用的初始值
即可作為右值使用,也可作為左值使用
C++鏈式編程中,經常用到引用,運算符重載專題
返回值是基礎類型,當引用
int getAA1()
{
int a;
a = 10;
return a;
}
//基礎類型a返回的時候,也會有一個副本
int& getAA2()
{
int a;
a = 10;
return a;
}
int* getAA3()
{
int a;
a = 10;
return &a;
}
返回值是static變量,當引用
//static修飾變量的時候,變量是一個狀態變量
int j()
{
static int a = 10;
a ++;
printf("a:%d \n", a);
return a;
}
int& j1()
{
static int a = 10;
a ++;
printf("a:%d \n", a);
return a;
}
int *j2()
{
static int a = 10;
a ++;
printf("a:%d \n", a);
return &a;
}
void main22()
{
// j()的運算結果是一個數值,沒有內存地址,不能當左值。。。。。
//11 = 100;
//*(a>b?&a:&b) = 111;
//當被調用的函數當左值的時候,必須返回一個引用。。。。。
j1() = 100; //編譯器幫我們打造了環境
j1();
*(j2()) = 200; //相當於我們程序員手工的打造 做左值的條件
j2();
system("pause");
}
返回值是形參,當引用
int g1(int *p)
{
*p = 100;
return *p;
}
int& g2(int *p) //
{
*p = 100;
return *p;
}
//當我們使用引用語法的時候 ,我們不去關心編譯器引用是怎麼做的
//當我們分析亂碼這種現象的時候,我們才去考慮c++編譯器是怎麼做的。。。。
void main23()
{
int a1 = 10;
a1 = g2(&a1);
int &a2 = g2(&a1); //用引用去接受函數的返回值,是不是亂碼,關鍵是看返回的內存空間是不是被編譯器回收了。。。。
printf("a1:%d \n", a1);
printf("a2:%d \n", a2);
system("pause");
}
返回值非基礎類型
struct Teachar
{
charname[64];
intage;
};
//如果返回引用不是基礎類型,是一個類,那麼情況非常賦值。。涉及到copy構造函數和=操作重載,拋磚。。。。
struct Teachar
{
char name[64];
int age;
};
//如果返回引用不是基礎類型,是一個類,那麼情況非常賦值。。涉及到copy構造函數和=操作重載,拋磚。。。。
struct Teachar & OpTeacher(struct Teachar &t1)
{
}
#include "iostream"
using namespace std;
struct Teacher
{
char name[64];
int age;
};
int getTe(Teacher **myp )
{
Teacher *p = (Teacher *)malloc(sizeof(Teacher));
if (p ==NULL)
{
return -1;
}
memset(p, 0, sizeof(Teacher));
p->age = 33;
*myp = p; //
return 0;
}
//指針的引用而已
int getTe2(Teacher* &myp)
{
myp = (Teacher *)malloc(sizeof(Teacher));
myp->age = 34;
return 0;
}
void main333()
{
Teacher *p = NULL;
//getTe(&p);
getTe2(p);
printf("age:%d \n", p->age);
system("pause");
}
下面開始進入const引用難點
思考cost int &a = b PK const int &a = 10;
????問題:const引用,
在C++中可以聲明const引用
const Type& name = var;
const引用讓變量擁有只讀屬性
案例1:
int main()
{
int a = 10;
const int &b = a;
//int *p = (int *)&b;
b = 11; //err
//*p = 11; //只能用指針來改變了
cout<<"b--->"<
printf("a:%d\n", a);
printf("b:%d\n", b);
printf("&a:%d\n", &a);
printf("&b:%d\n", &b);
system("pause");
return 0;
}
案例2:
void main41()
{
int a = 10;
const int &b = a; //const引用使用變量a初始化
a = 11;
//b = 12; //通過引用修改a,對不起修改不了
system("pause");
}
struct Teacher1
{
char name[64];
int age;
};
void printTe2(const Teacher1 *const pt)
{
}
//const引用讓變量(所指內存空間)擁有只讀屬性
void printTe(const Teacher1 &t)
{
//t.age = 11;
}
void main42()
{
Teacher1 t1;
t1.age = 33;
printTe(t1);
system("pause");
}
思考:
1、用變量對const引用初始化,const引用分配內存空間了嗎?
2、用常量對const引用初始化,const引用分配內存空間了嗎?
void main()
{
const int b = 10;
printf("b:%d", &b);
//int &a1 = 19; 如果不加const編譯失敗
const int &a = 19;
printf("&a:%d \n", &a);
system("pause");
}
void main()
{
//普通引用
int a = 10;
int &b = a;
//常量引用:讓變量引用只讀屬性
const int &c = a;
//常量引用初始化分為兩種
//1 用變量 初始化 常量引用
{
int x = 20;
const int& y = x;
printf("y:%d \n", y);
}
//2 用常量 初始化 常量引用
{
//int &m = 10; //引用是內存空間的別名 字面量10沒有內存空間 沒有方法做引用
const int &m = 10;
}
cout<<"hello..."<
system("pause");
return ;
}
1)Const &int e 相當於 const int * conste
2)普通引用 相當於 int *const e1
3)當使用常量(字面量)對const引用進行初始化時,C++編譯器會為常量值分配空間,並將引用名作為這段空間的別名
4)使用字面量對const引用初始化後,將生成一個只讀變量
後續課程介紹
int& j()
{
static int a = 0;
return a;
}
int& g()
{
int a = 0;
return a;
}
int main()
{
int a = g();
int& b = g();
j() = 10;
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("f() = %d\n", f());
system("pause");
return 0;
}
C++中的const常量可以替代宏常數定義,如:
const int A = 3; #define A 3
C++中是否有解決方案替代宏代碼片段呢?(替代宏代碼片段就可以避免宏的副作用!)
C++中推薦使用內聯函數替代宏代碼片段
C++中使用inline關鍵字聲明內聯函數
內聯函數聲明時inline關鍵字必須和函數定義結合在一起,否則編譯器會直接忽略內聯請求。
//宏替換和函數調用區別
#include "iostream"
using namespace std;
#define MYFUNC(a, b) ((a) < (b) ? (a) : (b))
inline int myfunc(int a, int b)
{
return a < b ? a : b;
}
int main()
{
int a = 1;
int b = 3;
//int c = myfunc(++a, b); //頭疼系統
int c = MYFUNC(++a, b);
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("c = %d\n", c);
system("pause");
return 0;
}
說明1:
必須inline int myfunc(int a, int b)和函數體的實現,寫在一塊
說明2
C++編譯器可以將一個函數進行內聯編譯
被C++編譯器內聯編譯的函數叫做內聯函數
內聯函數在最終生成的代碼中是沒有定義的
C++編譯器直接將函數體插入在函數調用的地方
內聯函數沒有普通函數調用時的額外開銷(壓棧,跳轉,返回)
說明3:C++編譯器不一定准許函數的內聯請求!
說明4
內聯函數是一種特殊的函數,具有普通函數的特征(參數檢查,返回類型等)
內聯函數是對編譯器的一種請求,因此編譯器可能拒絕這種請求
內聯函數由 編譯器處理,直接將編譯後的函數體插入調用的地方
宏代碼片段 由預處理器處理, 進行簡單的文本替換,沒有任何編譯過程
說明5:
現代C++編譯器能夠進行編譯優化,因此一些函數即使沒有inline聲明,也可能被編譯器內聯編譯
另外,一些現代C++編譯器提供了擴展語法,能夠對函數進行強制內聯
如:g++中的__attribute__((always_inline))屬性
說明6:
C++中內聯編譯的限制:
不能存在任何形式的循環語句
不能存在過多的條件判斷語句
函數體不能過於龐大
不能對函數進行取址操作
函數內聯聲明必須在調用語句之前
編譯器對於內聯函數的限制並不是絕對的,內聯函數相對於普通函數的優勢只是省去了函數調用時壓棧,跳轉和返回的開銷。
因此,當函數體的執行開銷遠大於壓棧,跳轉和返回所用的開銷時,那麼內聯將無意義。
結論:
1)內聯函數在編譯時直接將函數體插入函數調用的地方
2)inline只是一種請求,編譯器不一定允許這種請求
3)內聯函數省去了普通函數調用時壓棧,跳轉和返回的開銷
/*1
C++中可以在函數聲明時為參數提供一個默認值,
當函數調用時沒有指定這個參數的值,編譯器會自動用默認值代替
*/
void myPrint(int x = 3)
{
printf("x:%d", x);
}
/*2
函數默認參數的規則
只有參數列表後面部分的參數才可以提供默認參數值
一旦在一個函數調用中開始使用默認參數值,那麼這個參數後的所有參數都必須使用默認參數值
*/
//默認參數
void printAB(int x = 3)
{
printf("x:%d\n", x);
}
//在默認參數規則 ,如果默認參數出現,那麼右邊的都必須有默認參數
void printABC(int a, int b, int x = 3, int y=4, int z = 5)
{
printf("x:%d\n", x);
}
int main62(int argc, char *argv[])
{
printAB(2);
printAB();
system("pause");
return 0;
}
/*
函數占位參數
占位參數只有參數類型聲明,而沒有參數名聲明
一般情況下,在函數體內部無法使用占位參數
*/
int func(int a, int b, int )
{
return a + b;
}
int main01()
{
//func(1, 2); //可以嗎?
printf("func(1, 2, 3) = %d\n", func(1, 2, 3));
getchar();
return 0;
}
/*
可以將占位參數與默認參數結合起來使用
意義
為以後程序的擴展留下線索
兼容C語言程序中可能出現的不規范寫法
*/
//C++可以聲明占位符參數,占位符參數一般用於程序擴展和對C代碼的兼容
int func2(int a, int b, int = 0)
{
return a + b;
}
void main()
{
//如果默認參數和占位參數在一起,都能調用起來
func2(1, 2);
func2(1, 2, 3);
system("pause");
}
結論://如果默認參數和占位參數在一起,都能調用起來
1 函數重載概念
函數重載(Function Overload)
用同一個函數名定義不同的函數
當函數名和不同的參數搭配時函數的含義不同
2 函數重載的判斷標准
/*
函數重載至少滿足下面的一個條件:
參數個數不同
參數類型不同
參數順序不同
*/
3 函數返回值不是函數重載的判斷標准
實驗1:調用情況分析;實驗2:判斷標准
//兩個難點:重載函數和默認函數參數混搭 重載函數和函數指針
/*
int func(int x)
{
return x;
}
int func(int a, int b)
{
return a + b;
}
int func(const char* s)
{
return strlen(s);
}
int main()
{
int c = 0;
c = func(1);
printf("c = %d\n", c);
c = func(1, 2);
printf("c = %d\n", c);
c = func("12345");
printf("c = %d\n", c);
printf("Press enter to continue ...");
getchar();
return 0;
}
*/
/*
編譯器調用重載函數的准則
將所有同名函數作為候選者
嘗試尋找可行的候選函數
精確匹配實參
通過默認參數能夠匹配實參
通過默認類型轉換匹配實參
匹配失敗
最終尋找到的可行候選函數不唯一,則出現二義性,編譯失敗。
無法匹配所有候選者,函數未定義,編譯失敗。
*/
/*
函數重載的注意事項
重載函數在本質上是相互獨立的不同函數(靜態鏈編)
重載函數的函數類型是不同的
函數返回值不能作為函數重載的依據
函數重載是由函數名和參數列表決定的。
*/
函數重載是發生在一個類中裡面
//當函數默認參數遇上函數重載會發生什麼
/*
int func(int a, int b, int c = 0)
{
return a * b * c;
}
int func(int a, int b)
{
return a + b;
}
//1個參數的允許嗎
int func(int a)
{
return a + b;
}
int main()
{
int c = 0;
c = func(1, 2); // 存在二義性,調用失敗,編譯不能通過
printf("c = %d\n", c);
printf("Press enter to continue ...");
getchar();
return 0;
}
*/
/*
函數重載與函數指針
當使用重載函數名對函數指針進行賦值時
根據重載規則挑選與函數指針參數列表一致的候選者
嚴格匹配候選者的函數類型與函數指針的函數類型
*/
/*
int func(int x) // int(int a)
{
return x;
}
int func(int a, int b)
{
return a + b;
}
int func(const char* s)
{
return strlen(s);
}
typedef int(*PFUNC)(int a); // int(int a)
int main()
{
int c = 0;
PFUNC p = func;
c = p(1);
printf("c = %d\n", c);
printf("Press enter to continue ...");
getchar();
return 0;
}
*/
後續課程。
register:這個關鍵字請求編譯器盡可能的將變量存在CPU內部寄存器中,而不是通過內存尋址訪問,以提高效率。注意是盡可能,不是絕對。你想想,一個CPU 的寄存器也就那麼幾個或幾十個,你要是定義了很多很多register 變量,它累死也可能不能全部把這些變量放入寄存器吧,輪也可能輪不到你。
一、皇帝身邊的小太監----寄存器
不知道什麼是寄存器?那見過太監沒有?沒有?其實我也沒有。沒見過不要緊,見過就麻煩大了。^_^,大家都看過古裝戲,那些皇帝們要閱讀奏章的時候,大臣總是先將奏章交給皇帝旁邊的小太監,小太監呢再交給皇帝同志處理。這個小太監只是個中轉站,並無別的功能。
好,那我們再聯想到我們的CPU。CPU 不就是我們的皇帝同志麼?大臣就相當於我們的內存,數據從他這拿出來。那小太監就是我們的寄存器了(這裡先不考慮CPU 的高速緩存區)。數據從內存裡拿出來先放到寄存器,然後CPU 再從寄存器裡讀取數據來處理,處理完後同樣把數據通過寄存器存放到內存裡,CPU 不直接和內存打交道。這裡要說明的一點是:小太監是主動的從大臣手裡接過奏章,然後主動的交給皇帝同志,但寄存器沒這麼自覺,它從不主動干什麼事。一個皇帝可能有好些小太監,那麼一個CPU 也可以有很多寄存器,不同型號的CPU 擁有寄存器的數量不一樣。
為啥要這麼麻煩啊?速度!就是因為速度。寄存器其實就是一塊一塊小的存儲空間,只不過其存取速度要比內存快得多。進水樓台先得月嘛,它離CPU 很近,CPU 一伸手就拿到數據了,比在那麼大的一塊內存裡去尋找某個地址上的數據是不是快多了?那有人問既然它速度那麼快,那我們的內存硬盤都改成寄存器得了呗。我要說的是:你真有錢!
二、舉例
register修飾符暗示編譯程序相應的變量將被頻繁地使用,如果可能的話,應將其保存在CPU的寄存器中,以加快其存儲速度。例如下面的內存塊拷貝代碼,
#ifdef NOSTRUCTASSIGN
memcpy (d, s, l)
{
register char *d;
register char *s;
register int i;
while (i--)
*d++ = *s++;
}
#endif
三、使用register 修飾符的注意點
但是使用register修飾符有幾點限制。
首先,register變量必須是能被CPU所接受的類型。這通常意味著register變量必須是一個單個的值,並且長度應該小於或者等於整型的長度。不過,有些機器的寄存器也能存放浮點數。
其次,因為register變量可能不存放在內存中,所以不能用“&”來獲取register變量的地址。
由於寄存器的數量有限,而且某些寄存器只能接受特定類型的數據(如指針和浮點數),因此真正起作用的register修飾符的數目和類型都依賴於運行程序的機器,而任何多余的register修飾符都將被編譯程序所忽略。
在某些情況下,把變量保存在寄存器中反而會降低程序的運行速度。因為被占用的寄存器不能再用於其它目的;或者變量被使用的次數不夠多,不足以裝入和存儲變量所帶來的額外開銷。
早期的C編譯程序不會把變量保存在寄存器中,除非你命令它這樣做,這時register修飾符是C語言的一種很有價值的補充。然而,隨著編譯程序設計技術的進步,在決定那些變量應該被存到寄存器中時,現在的C編譯環境能比程序員做出更好的決定。實際上,許多編譯程序都會忽略register修飾符,因為盡管它完全合法,但它僅僅是暗示而不是命令。
9 作業及強化訓練
1 復雜數據類型引用做函數參數
分析內存四區變化圖
2 代碼敲一遍
3 設計一個類, 求圓形的周長
4 設計一個學生類,屬性有姓名和學號,
可以給姓名和學號賦值
可以顯示學生的姓名和學號
C++學習技術路線及目標
研究C++編譯器管理類和對象的方法===》避免死角
c++編譯器對類對象的生命周期管理,對象創建、使用、銷毀
c++面向對象模型初探
c++面向對象多態原理探究
操作符重載
C++基礎課程學習完畢以後,有沒有一個標准,來判斷自己有沒有入門。
面向抽象類(接口)編程
1)類、對象、成員變量、成員函數
2)面向對象三大概念
封裝、繼承、多態
3)編程實踐
類的定義和對象的定義,對象的使用
求圓形的面積
定義Teacher類,打印Teacher的信息(把類的聲明和類的實現分開)
1)封裝(Encapsulation)
A)封裝,是面向對象程序設計最基本的特性。把數據(屬性)和函數(操作)合成一個整體,這在計算機世界中是用類與對象實現的。
B)封裝,把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
備注:有2層含義(把屬性和方法進行封裝對屬性和方法進行訪問控制)
C++中類的封裝
成員變量,C++中用於表示類屬性的變量
成員函數,C++中用於表示類行為的函數
2)類成員的訪問控制
在C++中可以給成員變量和成員函數定義訪問級別
Public修飾成員變量和成員函數可以在類的內部和類的外部被訪問
Private修飾成員變量和成員函數只能在類的內部被訪問
//類是把屬性和方法封裝 同時對信息進行訪問控制
//類的內部,類的外部
//我們抽象了一個類,用類去定義對象
//類是一個數據類型,類是抽象的
//對象是一個具體的變量。。占用內存空間。
class Circle
{
public:
double r;
double s;
public:
double getR()
{
a++;
return r;
}
void setR(double val)
{
r = val;
}
public:
double getS() //增加功能時,是在修改類, 修改類中的屬性或者是方法
{
s = 3.14f*r*r;
return s;
}
//private:
int a;
};
3)struct和class關鍵字區別
在用struct定義類時,所有成員的默認屬性為public
在用class定義類時,所有成員的默認屬性為private
目標:面向過程向面向對象思想轉變
初學者要仔細體會類和對象之間的關系,並通過適當練習鞏固和提高!
案例1 設計立方體類(cube),求出立方體的面積和體積
求兩個立方體,是否相等(全局函數和成員函數)
案例2 設計一個圓形類(AdvCircle),和一個點類(Point),計算點在圓內部還是圓外
即:求點和圓的關系(圓內和圓外)
案例3 對於第二個案例,類的聲明和類的實現分開
作業1:編寫C++程序完成以下功能:
1)定義一個Point類,其屬性包括點的坐標,提供計算兩點之間距離的方法;
2)定義一個圓形類,其屬性包括圓心和半徑;
3)創建兩個圓形對象,提示用戶輸入圓心坐標和半徑,判斷兩個圓是否相交,並輸出結果。
作業2:設計並測試一個名為Rectangle的矩形類,其屬性為矩形的左下角與右上角兩個點的坐標,根據坐標能計算出矩形的面積
作業3:定義一個Tree類,有成員ages(樹齡),成員函數grow(int years)對ages加上years,age()顯示tree對象的ages的值。
前言
創建一個對象時,常常需要作某些初始化的工作,例如對數據成員賦初值。注意,類的數據成員是不能在聲明類時初始化的。
為了解決這個問題,C++編譯器提供了構造函數(constructor)來處理對象的初始化。構造函數是一種特殊的成員函數,與其他成員函數不同,不需要用戶來調用它,而是在建立對象時自動執行。
有關構造函數
1構造函數定義及調用
1)C++中的類可以定義與類名相同的特殊成員函數,這種與類名相同的成員函數叫做構造函數;
2)構造函數在定義時可以有參數;
3)沒有任何返回類型的聲明。
2構造函數的調用
自動調用:一般情況下C++編譯器會自動調用構造函數
手動調用:在一些情況下則需要手工調用構造函數
有關析構函數
3)析構函數定義及調用
1)C++中的類可以定義一個特殊的成員函數清理對象,這個特殊的成員函數叫做析構函數
語法:~ClassName()
2)析構函數沒有參數也沒有任何返回類型的聲明
3)析構函數在對象銷毀時自動被調用
4)析構函數調用機制
C++編譯器自動調用
代碼演示:dm01_構造函數的基礎.cpp
設計構造函數和析構函數的原因
面向對象的思想是從生活中來,手機、車出廠時,是一樣的。
生活中存在的對象都是被初始化後才上市的;初始狀態是對象普遍存在的一個狀態的
普通方案:
為每個類都提供一個public的initialize函數;
對象創建後立即調用initialize函數進行初始化。
優缺點分析
1)initialize只是一個普通的函數,必須顯示的調用
2)一旦由於失誤的原因,對象沒有初始化,那麼結果將是不確定的
沒有初始化的對象,其內部成員變量的值是不定的
3)不能完全解決問題
//為什麼對象需要初始化 有什麼樣的初始化方案
#include "iostream"
using namespace std;
/*
思考為什麼需要初始化
面向對象思想來自生活,手機、車、電子產品,出廠時有初始化
怎麼樣進行初始化?
方案1:顯示調用方法
缺點:易忘、麻煩;顯示調用init,不能完全解決問題
*/
class Test21
{
public:
int m;
int getM() const { return m; }
void setM(int val) { m = val; }
int n;
int getN() const { return n; }
void setN(int val) { n = val; }
public:
int init(int m,int n)
{
this->m = m;
this->n = n;
return 0;
}
protected:
private:
};
int main()
{
int rv =0;
Test21 t1; //無參構造函數的調用方法
Test21 t2;
//t1.init(100, 200);
//t2.init(300, 400);
cout<
cout<
//定義對象數組時,沒有機會進行顯示初始化
Test21 arr[3];
//Test arr_2[3] = {Test(1,3), Test(), Test()};
system("pause");
return rv;
}
C++編譯器給程序員提供的對象初始化方案,高端大氣上檔次。
//有參數構造函數的三種調用方法
class Test
{
private:
int a;
int b;
public:
//無參數構造函數
Test()
{
;
}
//帶參數的構造函數
Test(int a, int b)
{
;
}
//賦值構造函數
Test(const Test &obj)
{
;
}
public:
void init(int _a, int _b)
{
a = _a;
b = _b;
}
};
調用方法: Testt1, t2;
有參構造函數的三種調用方法
//有參數構造函數的三種調用方法
class Test5
{
private:
int a;
public:
//帶參數的構造函數
Test5(int a)
{
printf("\na:%d", a);
}
Test5(int a, int b)
{
printf("\na:%d b:%d", a, b);
}
public:
};
int main55()
{
Test5 t1(10); //c++編譯器默認調用有參構造函數 括號法
Test5 t2 = (20, 10); //c++編譯器默認調用有參構造函數 等號法
Test5 t3 = Test5(30); //程序員手工調用構造函數 產生了一個對象 直接調用構造構造函數法
system("pause");
return 0;
}
賦值構造函數的四種調用場景(調用時機)
第1和第2個調用場景
#include "iostream"
using namespace std;
class AA
{
public:
AA() //無參構造函數 默認構造函數
{
cout<<"我是構造函數,自動被調用了"<
}
AA(int _a) //無參構造函數 默認構造函數
{
a = _a;
}
AA(const AA &obj2)
{
cout<<"我也是構造函數,我是通過另外一個對象obj2,來初始化我自己"<
a = obj2.a + 10;
}
~AA()
{
cout<<"我是析構函數,自動被調用了"<
}
void getA()
{
printf("a:%d \n", a);
}
protected:
private:
int a;
};
//單獨搭建一個舞台
void ObjPlay01()
{
AA a1; //變量定義
//賦值構造函數的第一個應用場景
//用對象1 初始化 對象2
AA a2 = a1; //定義變量並初始化 //初始化法
a2 = a1; //用a1來=號給a2 編譯器給我們提供的淺copy
}
第二個應用場景
//單獨搭建一個舞台
void ObjPlay02()
{
AA a1(10); //變量定義
//賦值構造函數的第一個應用場景
//用對象1 初始化 對象2
AA a2(a1); //定義變量並初始化 //括號法
//a2 = a1; //用a1來=號給a2 編譯器給我們提供的淺copy
a2.getA();
}
//注意:初始化操作 和 等號操作 是兩個不同的概念
第3個調用場景
#include "iostream"
using namespace std;
class Location
{
public:
Location( int xx = 0 , int yy = 0 )
{
X = xx ; Y = yy ; cout << "Constructor Object.\n" ;
}
Location( const Location & p ) //拷貝構造函數
{
X = p.X ; Y = p.Y ; cout << "Copy_constructor called." << endl ;
}
~Location()
{
cout << X << "," << Y << " Object destroyed." << endl ;
}
int GetX () { return X ; } int GetY () { return Y ; }
private : int X , Y ;
} ;
//alt + f8 排版
void f ( Location p )
{
cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl ;
}
void mainobjplay()
{
Location A ( 1, 2 ) ; //形參是一個元素,函數調用,會執行實參變量初始化形參變量
f ( A ) ;
}
void main()
{
mainobjplay();
system("pause");
}
第4個調用場景
第四個應用場景
#include "iostream"
using namespace std;
class Location
{
public:
Location( int xx = 0 , int yy = 0 )
{
X = xx ; Y = yy ; cout << "Constructor Object.\n" ;
}
Location( const Location & p ) //復制構造函數
{
X = p.X ; Y = p.Y ; cout << "Copy_constructor called." << endl ;
}
~Location()
{
cout << X << "," << Y << " Object destroyed." << endl ;
}
int GetX () { return X ; } int GetY () { return Y ; }
private : int X , Y ;
} ;
//alt + f8 排版
void f ( Location p )
{
cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl ;
}
Location g()
{
Location A(1, 2);
return A;
}
//對象初始化操作 和 =等號操作 是兩個不同的概念
//匿名對象的去和留,關鍵看,返回時如何接
void mainobjplay()
{
//若返回的匿名對象,賦值給另外一個同類型的對象,那麼匿名對象會被析構
//Location B;
//B = g(); //用匿名對象賦值給B對象,然後匿名對象析構
//若返回的匿名對象,來初始化另外一個同類型的對象,那麼匿名對象會直接轉成新的對象
Location B = g();
cout<<"傳智掃地僧測試"<
}
void main()
{
mainobjplay();
system("pause");
}
二個特殊的構造函數
1)默認無參構造函數
當類中沒有定義構造函數時,編譯器默認提供一個無參構造函數,並且其函數體為空
2)默認拷貝構造函數
當類中沒有定義拷貝構造函數時,編譯器默認提供一個默認拷貝構造函數,簡單的進行成員變量的值復制
1)當類中沒有定義任何一個構造函數時,c++編譯器會提供默認無參構造函數和默認拷貝構造函數
2)當類中定義了拷貝構造函數時,c++編譯器不會提供無參數構造函數
3) 當類中定義了任意的非拷貝構造函數(即:當類中提供了有參構造函數或無參構造函數),c++編譯器不會提供默認無參構造函數
4 )默認拷貝構造函數成員變量簡單賦值
總結:只要你寫了構造函數,那麼你必須用。
構造析構階段性總結
1)構造函數是C++中用於初始化對象狀態的特殊函數
2)構造函數在對象創建時自動被調用
3)構造函數和普通成員函數都遵循重載規則
4)拷貝構造函數是對象正確初始化的重要保證
5)必要的時候,必須手工編寫拷貝構造函數
========》1個對象的初始化講完了,增加一個案例。
? 默認復制構造函數可以完成對象的數據成員值簡單的復制
? 對象的數據資源是由指針指示的堆時,默認復制構造函數僅作指針值復制
深拷貝淺拷貝現象出現的原因
顯示提供copy構造函數
顯示操作重載=號操作,不使用編譯器提供的淺copy
class Name
{
public:
Name(const char *pname)
{
size = strlen(pname);
pName = (char *)malloc(size + 1);
strcpy(pName, pname);
}
Name(Name &obj)
{
//用obj來初始化自己
pName = (char *)malloc(obj.size + 1);
strcpy(pName, obj.pName);
size = obj.size;
}
~Name()
{
cout<<"開始析構"<
if (pName!=NULL)
{
free(pName);
pName = NULL;
size = 0;
}
}
void operator=(Name &obj3)
{
if (pName != NULL)
{
free(pName);
pName = NULL;
size = 0;
}
cout<<"測試有沒有調用我。。。。"<
//用obj3來=自己
pName = (char *)malloc(obj3.size + 1);
strcpy(pName, obj3.pName);
size = obj3.size;
}
protected:
private:
char *pName;
int size;
};
//對象的初始化 和 對象之間=號操作是兩個不同的概念
void playObj()
{
Name obj1("obj1.....");
Name obj2 = obj1; //obj2創建並初始化
Name obj3("obj3...");
//重載=號操作符
obj2 = obj3; //=號操作
cout<<"業務操作。。。5000"<
}
void main61()
{
playObj();
system("pause");
}
1)對象初始化列表出現原因
1.必須這樣做:
如果我們有一個類成員,它本身是一個類或者是一個結構,而且這個成員它只有一個帶參數的構造函數,沒有默認構造函數。這時要對這個類成員進行初始化,就必須調用這個類成員的帶參數的構造函數,
如果沒有初始化列表,那麼他將無法完成第一步,就會報錯。
2、類成員中若有const修飾,必須在對象初始化的時候,給const int m 賦值
當類成員中含有一個const對象時,或者是一個引用時,他們也必須要通過成員初始化列表進行初始化,
因為這兩種對象要在聲明後馬上初始化,而在構造函數中,做的是對他們的賦值,這樣是不被允許的。
2)C++中提供初始化列表對成員變量進行初始化
語法規則
Constructor::Contructor() : m1(v1),m2(v1,v2), m3(v3)
{
// some other assignment operation
}
3)注意概念
初始化:被初始化的對象正在創建
賦值:被賦值的對象已經存在
4)注意:
成員變量的初始化順序與聲明的順序相關,與在初始化列表中的順序無關
初始化列表先於構造函數的函數體執行
/*
1 C++中提供了初始化列表對成員變量進行初始化
2 使用初始化列表出現原因:
1.必須這樣做:
如果我們有一個類成員,它本身是一個類或者是一個結構,而且這個成員它只有一個帶參數的構造函數,
而沒有默認構造函數,這時要對這個類成員進行初始化,就必須調用這個類成員的帶參數的構造函數,
如果沒有初始化列表,那麼他將無法完成第一步,就會報錯。
2、類成員中若有const修飾,必須在對象初始化的時候,給const int m 賦值
當類成員中含有一個const對象時,或者是一個引用時,他們也必須要通過成員初始化列表進行初始化,
因為這兩種對象要在聲明後馬上初始化,而在構造函數中,做的是對他們的賦值,這樣是不被允許的。
*/
//總結 構造和析構的調用順序
#include "iostream"
using namespace std;
class ABC
{
public:
ABC(int a, int b, int c)
{
this->a = a;
this->b = b;
this->c = c;
printf("a:%d,b:%d,c:%d \n", a, b, c);
printf("ABC construct ..\n");
}
~ABC()
{
printf("a:%d,b:%d,c:%d \n", a, b, c);
printf("~ABC() ..\n");
}
protected:
private:
int a;
int b;
int c;
};
class MyD
{
public:
MyD():abc1(1,2,3),abc2(4,5,6),m(100)
//MyD()
{
cout<<"MyD()"<
}
~MyD()
{
cout<<"~MyD()"<
}
protected:
private:
ABC abc1; //c++編譯器不知道如何構造abc1
ABC abc2;
const int m;
};
int run()
{
MyD myD;
return 0;
}
int main_dem03()
{
run();
system("pause");
return 0;
}
構造函數與析構函數的調用順序
1)當類中有成員變量是其它類的對象時,首先調用成員變量的構造函數,調用順序與聲明順序相同;之後調用自身類的構造函數
2)析構函數的調用順序與對應的構造函數調用順序相反
通過訓練,把所學知識點都穿起來
demo10_構造析構練習強化.cpp (講解)
展示分析過程,注意賦值構函數的調用
demo10_構造析構練習強化.cpp
1) 匿名對象生命周期
2) 匿名對象的去和留
3) 構造中調用構造
demo11_匿名對象練習強化.cpp
構造函數中調用構造函數,是一個蹩腳的行為。
1)在軟件開發過程中,常常需要動態地分配和撤銷內存空間,例如對動態鏈表中結點的插入與刪除。在C語言中是利用庫函數malloc和free來分配和撤銷內存空間的。C++提供了較簡便而功能較強的運算符new和delete來取代malloc和free函數。
注意: new和delete是運算符,不是函數,因此執行效率高。
2)雖然為了與C語言兼容,C++仍保留malloc和free函數,但建議用戶不用malloc和free函數,而用new和delete運算符。new運算符的例子:
new int; //開辟一個存放整數的存儲空間,返回一個指向該存儲空間的地址(即指針)
new int(100); //開辟一個存放整數的空間,並指定該整數的初值為100,返回一個指向該存儲空間的地址
new char[10]; //開辟一個存放字符數組(包括10個元素)的空間,返回首元素的地址
new int[5][4]; //開辟一個存放二維整型數組(大小為5*4)的空間,返回首元素的地址
float *p=new float (3.14159); //開辟一個存放單精度數的空間,並指定該實數的初值為//3.14159,將返回的該空間的地址賦給指針變量p
3)new和delete運算符使用的一般格式為:
用new分配數組空間時不能指定初值。如果由於內存不足等原因而無法正常分配空間,則new會返回一個空指針NULL,用戶可以根據該指針的值判斷分配空間是否成功。
4)應用舉例
使用類名定義的對象都是靜態的,在程序運行過程中,對象所占的空間是不能隨時釋放的。但有時人們希望在需要用到對象時才建立對象,在不需要用該對象時就撤銷它,釋放它所占的內存空間以供別的數據使用。這樣可提高內存空間的利用率。
C++中,可以用new運算符動態建立對象,用delete運算符撤銷對象
比如:
Box *pt; //定義一個指向Box類對象的指針變量pt
pt=new Box; //在pt中存放了新建對象的起始地址
在程序中就可以通過pt訪問這個新建的對象。如
cout<
cout<
C++還允許在執行new時,對新建立的對象進行初始化。如
Box *pt=new Box(12,15,18);
這種寫法是把上面兩個語句(定義指針變量和用new建立新對象)合並為一個語句,並指定初值。這樣更精煉。
新對象中的height,width和length分別獲得初值12,15,18。調用對象既可以通過對象名,也可以通過指針。
在執行new運算時,如果內存量不足,無法開辟所需的內存空間,目前大多數C++編譯系統都使new返回一個0指針值。只要檢測返回值是否為0,就可判斷分配內存是否成功。
ANSI C++標准提出,在執行new出現故障時,就“拋出”一個“異常”,用戶可根據異常進行有關處理。但C++標准仍然允許在出現new故障時返回0指針值。當前,不同的編譯系統對new故障的處理方法是不同的。
在不再需要使用由new建立的對象時,可以用delete運算符予以釋放。如
delete pt; //釋放pt指向的內存空間
這就撤銷了pt指向的對象。此後程序不能再使用該對象。
如果用一個指針變量pt先後指向不同的動態對象,應注意指針變量的當前指向,以免刪錯了對象。在執行delete運算符時,在釋放內存空間之前,自動調用析構函數,完成有關善後清理工作。
//1 mallocfree函數 c關鍵字
// newdelete 操作符號 c++的關鍵字
//2 new 在堆上分配內存 delete
//分配基礎類型 、分配數組類型、分配對象
//3 new和malloc 深入分析
混用測試、異同比較
結論: malloc不會調用類的構造函數
Free不會調用類的析構函數
思考:每個變量,擁有屬性。有沒有一些屬性,歸所有對象擁有?
1)定義靜態成員變量
? 關鍵字 static 可以用於說明一個類的成員,
靜態成員提供了一個同類對象的共享機制
? 把一個類的成員說明為 static 時,這個類無論有多少個對象被創建,這些對象共享這個 static 成員
? 靜態成員局部於類,它不是對象成員
例如:
#include
using namespace std;
classcounter
{
static intnum ; //聲明與定義靜態數據成員
public :
void setnum ( int i ) { num = i ;} //成員函數訪問靜態數據成員
void shownum() { cout <
} ;
intcounter :: num = 0 ;//聲明與定義靜態數據成員
void main ()
{ counter a , b ;
a.shownum() ; //調用成員函數訪問私有靜態數據成員
b.shownum() ;
a.setnum(10) ;
a.shownum() ;
b.shownum() ;
}
從結果可以看出,訪問的是同一個靜態數據成員
2)使用靜態成員變量
// 例5-14 使用公有靜態數據成員
#include
classcounter
{ public :
counter (int a) { mem = a; }
int mem; //公有數據成員
static int Smem ; //公有靜態數據成員
} ;
intcounter :: Smem = 1 ; //初始值為1
void main()
{ counter c(5);
int i ;
for( i = 0 ; i < 5 ; i ++ )
{ counter::Smem += i ;
cout << counter::Smem << '\t' ; //訪問靜態成員變量方法2
}
cout<
cout<<"c.Smem = "<<c.Smem<
cout<<"c.mem = "<<c.mem<
}
1)概念
? 靜態成員函數數冠以關鍵字static
? 靜態成員函數提供不依賴於類數據結構的共同操作,它沒有this指針
? 在類外調用靜態成員函數用“類名 :: ”作限定詞,或通過對象調用
2)案例
3)疑難問題:靜態成員函數中,不能使用普通變量。
//靜態成員變量屬於整個類的,分不清楚,是那個具體對象的屬性。
前言
C++對象模型可以概括為以下2部分:
1. 語言中直接支持面向對象程序設計的部分,主要涉及如構造函數、析構函數、虛函數、繼承(單繼承、多繼承、虛繼承)、多態等等。
2. 對於各種支持的底層實現機制。
在c語言中,“數據”和“處理數據的操作(函數)”是分開來聲明的,也就是說,語言本身並沒有支持“數據和函數”之間的關聯性。在c++中,通過抽象數據類型(abstractdata type,ADT),在類中定義數據和函數,來實現數據和函數直接的綁定。
概括來說,在C++類中有兩種成員數據:static、nonstatic;三種成員函數:static、nonstatic、virtual。
C++中的class從面向對象理論出發,將變量(屬性)和函數(方法)集中定義在一起,用於描述現實世界中的類。從計算機的角度,程序依然由數據段和代碼段構成。
C++編譯器如何完成面向對象理論到計算機程序的轉化?
換句話:C++編譯器是如何管理類、對象、類和對象之間的關系
具體的說:具體對象調用類中的方法,那,c++編譯器是如何區分,是那個具體的類,調用這個方法那?
思考一下程序結果
#include "iostream"
using namespace std;
class C1
{
public:
int i; //4
int j; //4
int k; //4
protected:
private:
}; //12
class C2
{
public:
int i; //4
int j; //4
int k; //4
static int m; //4
public:
int getK() const { return k; } //4
void setK(int val) { k = val; } //4
protected:
private:
}; //12 16 24
struct S1
{
int i;
int j;
int k;
}; //
struct S2
{
int i;
int j;
int k;
static int m;
}; //
int main()
{
printf("c1:%d \n", sizeof(C1));
printf("c2:%d \n", sizeof(C2));
printf("s1:%d \n", sizeof(S1));
printf("s2:%d \n", sizeof(S2));
system("pause");
}
通過上面的案例,我們可以的得出:
1)C++類對象中的成員變量和成員函數是分開存儲的
成員變量:
普通成員變量:存儲於對象中,與struct變量有相同的內存布局和字節對齊方式
靜態成員變量:存儲於全局數據區中
成員函數:存儲於代碼段中。
問題出來了:很多對象共用一塊代碼?代碼是如何區分具體對象的那?
換句話說:int getK() const { return k; },代碼是如何區分,具體obj1、obj2、obj3對象的k值?
2)C++編譯器對普通成員函數的內部處理
請仔細思考,並說出你的總結!
1、C++類對象中的成員變量和成員函數是分開存儲的。C語言中的內存四區模型仍然有效!
2、C++中類的普通成員函數都隱式包含一個指向當前對象的this指針。
3、靜態成員函數、成員變量屬於類
靜態成員函數與普通成員函數的區別
靜態成員函數不包含指向具體對象的指針
普通成員函數包含一個指向具體對象的指針
實驗1:若類成員函數的形參和 類的屬性,名字相同,通過this指針來解決。
實驗2:類的成員函數可通過const修飾,請問const修飾的是誰
1、把全局函數轉化成成員函數,通過this指針隱藏左操作數
Testadd(Test &t1, Test &t2)===》Test add( Test&t2)
2、把成員函數轉換成全局函數,多了一個參數
voidprintAB()===》void printAB(Test *pthis)
3、函數返回元素和返回引用
Test& add(Test &t2) //*this //函數返回引用
{
this->a = this->a +t2.getA();
this->b = this->b + t2.getB();
return*this; //*操作讓this指針回到元素狀態
}
Test add2(Test &t2)//*this //函數返回元素
{
//t3是局部變量
Test t3(this->a+t2.getA(),this->b + t2.getB()) ;
return t3;
}
voidadd3(Test &t2) //*this //函數返回元素
{
//t3是局部變量
Test t3(this->a+t2.getA(),this->b + t2.getB()) ;
//return t3;
}
例如
class A1
{
public:
A1()
{
a1 = 100;
a2 = 200;
}
int getA1()
{
return this->a1;
}
//聲明一個友元函數
friend void setA1(A1 *p, int a1); //這個函數是這個類的好朋友
protected:
private:
int a1;
int a2;
};
void setA1(A1 *p, int a1)
{
p->a1 = a1;
}
void main()
{
A1 mya1;
cout<
setA1(&mya1, 300); //通過友元函數 修改A類的私有屬性
cout<
system("pause");
}
? 若B類是A類的友員類,則B類的所有成員函數都是A類的友員函數
? 友員類通常設計為一種對數據操作或類之間傳遞消息的輔助類
? 某商店經銷一種貨物。貨物購進和賣出時以箱為單位,各箱的重量不一樣,因此,商店需要記錄目前庫存的總重量。現在用C++模擬商店貨物購進和賣出的情況。
#include "iostream"
using namespace std;
class Goods
{
public :
Goods ( int w) { weight = w ; total_weight += w ; }
~ Goods() { total_weight -= weight ; }
int Weight() { return weight ; } ;
static int TotalWeight() { return total_weight ; }
Goods *next ;
private :
int weight ;
static int total_weight ;
} ;
int Goods::total_weight = 0 ;
//r尾部指針
void purchase( Goods * &f, Goods *& r, int w )
{
Goods *p = new Goods(w) ;
p -> next = NULL ;
if ( f == NULL ) f = r = p ;
else { r -> next = p ; r = r -> next ; } //尾部指針下移或新結點變成尾部結點
}
void sale( Goods * & f , Goods * & r )
{
if ( f == NULL ) { cout << "No any goods!\n" ; return ; }
Goods *q = f ; f = f -> next ; delete q ;
cout << "saled.\n" ;
}
void main()
{
Goods * front = NULL , * rear = NULL ;
int w ; int choice ;
do
{
cout << "Please choice:\n" ;
cout << "Key in 1 is purchase,\nKey in 2 is sale,\nKey in 0 is over.\n" ;
cin >> choice ;
switch ( choice ) // 操作選擇
{
case 1 : // 鍵入1,購進1箱貨物
{ cout << "Input weight: " ;
cin >> w ;
purchase( front, rear, w ) ; // 從表尾插入1個結點
break ;
}
case 2 : // 鍵入2,售出1箱貨物
{ sale( front, rear ) ; break ; } // 從表頭刪除1個結點
case 0 : break ; // 鍵入0,結束
}
cout << "Now total weight is:" << Goods::TotalWeight() << endl ;
} while ( choice ) ;
}
目標:解決實際問題,訓練構造函數、copy構造函數等,為操作符重載做准備
數組類的測試
#include "iostream"
#include "Array.h"
using namespace std;
int main()
{
Array a1(10);
for(int i=0; i
{
a1.setData(i, i);
}
for(int i=0; i
{
printf("array %d: %d\n", i, a1.getData(i));
}
Array a2 = a1;
for(int i=0; i
{
printf("array %d: %d\n", i, a2.getData(i));
}
system("pause");
return 0;
}
數組類的頭文件
#ifndef _MYARRAY_H_
#define _MYARRAY_H_
class Array
{
private:
int mLength;
int* mSpace;
public:
Array(int length);
Array(const Array& obj);
int length();
void setData(int index, int value);
int getData(int index);
~Array();
};
#endif
? 類通常用關鍵字class定義。類是數據成員和成員函數的封裝。類的實例稱為對象。
? 結構類型用關鍵字struct定義,是由不同類型數據組成的數據類型。
? 類成員由private, protected, public決定訪問特性。public成員集稱為接口。
? 構造函數在創建和初始化對象時自動調用。析構函數則在對象作用域結束時自動調用。
? 重載構造函數和復制構造函數提供了創建對象的不同初始化方式。
? 靜態成員是局部於類的成員,提供一種同類對象的共享機制。
? 友員用關鍵字friend聲明。友員是對類操作的一種輔助手段。一個類的友員可以訪問該類各種性質的成員。
? 鏈表是一種重要的動態數據結構,可以在程序運行時創建或撤消數據元素。
所謂重載,就是重新賦予新的含義。函數重載就是對一個已有的函數賦予新的含義,使之實現新功能,因此,一個函數名就可以用來代表不同功能的函數,也就是”一名多用”。
運算符也可以重載。實際上,我們已經在不知不覺之中使用了運算符重載。例如,大家都已習慣於用加法運算符”+”對整數、單精度數和雙精度數進行加法運算,如5+8, 5.8 +3.67等,其實計算機對整數、單精度數和雙精度數的加法操作過程是很不相同的,但由於C++已經對運算符”+”進行了重載,所以就能適用於int, float, doUble類型的運算。
又如”<<“是C++的位運算中的位移運算符(左移),但在輸出操作中又是與流對象cout 配合使用的流插入運算符,”>>“也是位移運算符(右移),但在輸入操作中又是與流對象 cin 配合使用的流提取運算符。這就是運算符重載(operator overloading)。C++系統對”<<“和”>>“進行了重載,用戶在不同的場合下使用它們時,作用是不同的。對”<<“和”>>“的重載處理是放在頭文件stream中的。因此,如果要在程序中用”<< “和”>>”作流插入運算符和流提取運算符,必須在本文件模塊中包含頭文件stream(當然還應當包括”using namespace std“)。
現在要討論的問題是:用戶能否根據自己的需要對C++已提供的運算符進行重載,賦予它們新的含義,使之一名多用。?
1為什麼會用運算符重載機制
用復數類舉例
//Complex c3 =c1 + c2;
//原因 Complex是用戶自定義類型,編譯器根本不知道如何進行加減
//編譯器給提供了一種機制,讓用戶自己去完成,自定義類型的加減操作。。。。。
//這個機制就是運算符重載機制
2 運算符重載的本質是一個函數
class Complex
{
public:
int a;
int b;
friend Complex operator+(Complex &c1, Complex &c2);
public:
Complex(int a=0, int b=0)
{
this->a = a;
this->b = b;
}
public:
void printCom()
{
cout<
}
private:
};
/*
Complex myAdd(Complex &c1, Complex &c2)
{
Complex tmp(c1.a+ c2.a, c1.b + c2.b);
return tmp;
}
*/
Complex operator+(Complex &c1, Complex &c2)
{
Complex tmp(c1.a+ c2.a, c1.b + c2.b);
return tmp;
}
void main()
{
Complex c1(1, 2), c2(3, 4);
//Complex c3 = c1 + c2; //用戶自定義類型 編譯器無法讓變量相加
//Complex myAdd(Complex &c1, Complex &c2);
//1 普通函數
//Complex c3 = myAdd(c1, c2);
//c3.printCom();
//2 operator+ 函數名稱
//Complex c3 = operator+(c1, c2);
//c3.printCom();
//3 +替換 函數名
Complex c3 = c1 + c2; //思考C++編譯器如何支持操作符重載機制的 (根據類型)
c3.printCom();
{
int a =0, b = 0, c; //基礎類型C++編譯器知道如何加減
c = a +b;
}
//4 把Complex類變成私有屬性
//友元函數的應用場景
//friend Complex operator+(Complex &c1, Complex &c2);
cout<<"hello..."<
system("pause");
return ;
}
例如:
//全局函數 完成 +操作符 重載
Complex operator+(Complex &c1, Complex&c2)
//類成員函數 完成 -操作符 重載
Complex operator-(Complex &c2)
例如1:
//通過類成員函數完成-操作符重載
//函數聲明 Complexoperator-(Complex &c2)
//函數調用分析
//用類成員函數實現-運算符重載
Complex c4 = c1 - c2;
c4.printCom();
//c1.operator-(c2);
例如2:
//通過全局函數方法完成+操作符重載
//函數聲明 Complexoperator+(Complex &c1, Complex &c2)
//函數調用分析
int main()
{
Complex c1(1, 2), c2(3, 4);
//Complex c31 = operator+(c1,c2);
Complex c3 = c1 + c2;
c3.printCom();
}
例如3: 學員自己練習 實現 * /
例如3
//前置++操作符 用全局函數實現
Complex& operator++(Complex&c1)
{
c1.a ++;
c1.b ++;
return c1;
}
//調用方法
++c1 ; //=è需要寫出操作符重載函數原形
c1.printCom();
//運算符重載函數名定義
//首先承認操作符重載是一個函數 定義函數名èoperator++
//分析函數參數 根據左右操作數的個數,èoperator++(Complex &c1)
//分析函數返回值è Complex&operator++(Complex &c1) 返回它自身
例如4
//4.1前置—操作符 成員函數實現
Complex&operator--()
{
this->a--;
this->b--;
return *this;
}
//4.2調用方法
--c1;
c1.printCom();
//4.3前置—運算符重載函數名定義
//c1.operator--()
例如5
//5.1//後置++ 操作符用全局函數實現
Complex operator++(Complex &c1, int)
{
Complex tmp = c1;
c1.a++;
c1.b++;
return tmp;
}
//5.2 調用方法
c1 ++ ; //先使用後++
//5.3 後置++運算符重載函數名定義
Complex operator++(Complex&c1, int) //函數占位參數 和 前置++ 相區別
例如6
//6.1 後置— 操作符 用類成員函數實現
Complexoperator--(int)
{
Complextmp = *this;
this->a--;
this->b--;
returntmp;
}
//6.2 調用方法
c1 ++ ; //先使用後++
//6.3 後置--運算符重載函數名定義
Complex operator--(int) //函數占位參數和 前置-- 相區別
前置和後置運算符總結
C++中通過一個占位參數來區分前置運算和後置運算
全局函數、類成員函數方法實現運算符重載步驟
1)要承認操作符重載是一個函數,寫出函數名稱operator+ ()
2)根據操作數,寫出函數參數
3)根據業務,完善函數返回值(看函數是返回引用 還是指針 元素),及實現函數業務
1)友元函數和成員函數選擇方法
? 當無法修改左操作數的類時,使用全局函數進行重載
? =, [], ()和->操作符只能通過成員函數進行重載
2)用友元函數重載 << >>操作符
? istream 和 ostream 是 C++ 的預定義流類
? cin 是 istream 的對象,cout 是 ostream 的對象
? 運算符 << 由ostream 重載為插入操作,用於輸出基本類型數據
? 運算符 >> 由 istream 重載為提取操作,用於輸入基本類型數據
? 用友員函數重載 << 和 >> ,輸出和輸入用戶自定義的數據類型
a)用全局函數方法實現 << 操作符
ostream& operator<<(ostream &out, Complex &c1)
{
//out<<"12345,生活真是苦"<
out<
return out;
}
//調用方法
cout<
//鏈式編程支持
cout<
//cout.operator<<(c1).operator<<("abcd");
//函數返回值充當左值 需要返回一個引用
b)類成員函數方法無法實現 <<操作符重載
//因拿到cout這個類的源碼
//cout.operator<<(c1);
3) 友元函數重載操作符使用注意點
a) 友員函數重載運算符常用於運算符的左右操作數類型不同的情況
b)其他
? 在第一個參數需要隱式轉換的情形下,使用友員函數重載運算符是正確的選擇
? 友員函數沒有 this 指針,所需操作數都必須在參數表顯式聲明,很容易實現類型的隱式轉換
? C++中不能用友員函數重載的運算符有
=() []->
4 )友元函數案例vector類
#include
using namespace std;
//為vector類重載流插入運算符和提取運算符
class vector
{
public :
vector( int size =1 ) ;
~vector() ;
int & operator[]( int i ) ;
friend ostream & operator << ( ostream & output , vector & ) ;
friend istream & operator >> ( istream & input, vector & ) ;
private :
int * v ;
int len ;
};
vector::vector( int size )
{
if (size <= 0 || size > 100 )
{
cout << "The size of " << size << " is null !\n" ; abort() ;
}
v = new int[ size ] ; len = size ;
}
vector :: ~vector()
{
delete[] v ;
len = 0 ;
}
int &vector::operator[]( int i )
{
if( i >=0 && i < len ) return v[ i ] ;
cout << "The subscript " << i << " is outside !\n" ; abort() ;
}
ostream & operator << ( ostream & output, vector & ary )
{
for(int i = 0 ; i < ary.len ; i ++ )
output << ary[ i ] << " " ;
output << endl ;
return output ;
}
istream & operator >> ( istream & input, vector & ary )
{
for( int i = 0 ; i < ary.len ; i ++ )
input >> ary[ i ] ;
return input ;
}
void main()
{
int k ;
cout << "Input the length of vector A :\n" ;
cin >> k ;
vector A( k ) ;
cout << "Input the elements of vector A :\n" ;
cin >> A ;
cout << "Output the elements of vector A :\n" ;
cout << A ;
system("pause");
}
C++編譯器是如何支持操作符重載機制的?
? 賦值運算符重載用於對象數據的復制
? operator= 必須重載為成員函數
? 重載函數原型為:
類型 & 類名 :: operator= ( const 類名 & ) ;
案例:完善Name類,支持=號操作。
結論:
1//先釋放舊的內存
2返回一個引用
3=操作符 從右向左
//obj3 = obj1; // C++編譯器提供的 等號操作 也屬 淺拷貝
// obj4 = obj3 = obj1
//obj3.operator=(obj1)
Name& operator=(Name &obj1)
{
//1 先釋放obj3舊的內存
if (this->m_p != NULL)
{
delete[] m_p;
m_len = 0;
}
//2 根據obj1分配內存大小
this->m_len = obj1.m_len;
this->m_p = new char [m_len+1];
//3把obj1賦值給obj3
strcpy(m_p, obj1.m_p);
return *this;
}
重載[]和()運算符
? 運算符 [] 和 () 是二元運算符
? [] 和 () 只能用成員函數重載,不能用友元函數重載
重載下標運算符 []
[] 運算符用於訪問數據對象的元素
重載格式 類型 類 ::operator[] ( 類型 ) ;
設 x 是類 X 的一個對象,則表達式
x [ y ]
可被解釋為
x . operator [ ] ( y )
() 運算符用於函數調用
重載格式 類型 類 ::operator() ( 表達式表 ) ;
例1
設 x 是類 X 的一個對象,則表達式
x( arg1, arg2, … )
可被解釋為
x. operator () (arg1, arg2, … )
案例:
//例2:用重載()運算符實現數學函數的抽象
#include
classF
{public :
double operator( ) ( double x , doubley ) ;
} ;
doubleF :: operator ( ) ( doublex , double y )
{return x * x + y * y ; }
void main ( )
{
F f ;
f.getA();
cout << f (5.2 , 2.5 ) << endl ; // f . operator() (5.2, 2.5)
}
比較普通成員函數
//例3 用重載()運算符實現 pk 成員函數
#include
classF
{public :
double memFun ( double x, doubley ) ;
} ;
doubleF :: memFun ( double x, doubley )
{return x * x + y * y ; }
void main ( )
{
F f ;
cout << f.memFun( 5.2 , 2.5 ) << endl ;
}
理論知識:
1)&&和||是C++中非常特殊的操作符
2)&&和||內置實現了短路規則
3)操作符重載是靠函數重載來完成的
4)操作數作為函數參數傳遞
5)C++的函數參數都會被求值,無法實現短路規則
#include
#include
using namespace std;
class Test
{
int i;
public:
Test(int i)
{
this->i = i;
}
Test operator+ (const Test& obj)
{
Test ret(0);
cout<<"執行+號重載函數"<
ret.i = i + obj.i;
return ret;
}
bool operator&& (const Test& obj)
{
cout<<"執行&&重載函數"<
return i && obj.i;
}
};
// && 從左向右
void main()
{
int a1 = 0;
int a2 = 1;
cout<<"注意:&&操作符的結合順序是從左向右"<
if( a1 && (a1 + a2) )
{
cout<<"有一個是假,則不在執行下一個表達式的計算"<
}
Test t1 = 0;
Test t2 = 1;
If ( t1 && (t1 + t2) )
{
=è
T1.operator&&( t1 + t2) )
T1.operator&&( t1.operator+(t2) )
//t1 && t1.operator+(t2)
// t1.operator( t1.operator(t2) )
cout<<"兩個函數都被執行了,而且是先執行了+"<
}
system("pause");
return ;
}
添加<< >>
構造函數要求
//C語言中 沒有字符串這種類型,是通過數組來模擬字符串
//C++中 我們來設計一個字符串類 以零結尾的字符串
//若len為0,表示空串
MyString a; //空串“”
MyString a(“dddd”);
MyString b = a;
b = “aaaaaa”
b = a;
if (a > b)
if (a == b)
b[i] = ‘a’;
常用的操作符
<< >> !=== > < =
//C語言中 沒有字符串這種類型,是通過數組來模擬字符串
//C++中 我們來設計一個字符串 以零結尾的字符串
class MyString
{
friend ostream& operator<<(ostream &out, const MyString &s);
public: //構造和析構
MyString(int len = 0);
MyString(const char *p);
MyString(const MyString& obj);
~MyString();
public: //操作符重載
MyString& operator=(const char *p);
MyString& operator=(const MyString& obj);
char& operator[](int index) const;
public:
bool operator==(const char* p) const;
bool operator!=(const char* p) const;
bool operator==(const MyString& s) const;
bool operator!=(const MyString& s) const;
public: //string to c
char *c_str();
const char* c_str() const;
int length()
{
return m_len;
}
public:
int operator<(const char *p);
int operator>(const char *p);
int operator<(const MyString &s);
int operator>(const MyString &s);
private:
int m_len;
char *m_p;
};
1問題拋出
指針使用過程中,經常會出現內存洩漏和內存多次被釋放常
2 解決方案:例如:boost庫的智能指針
項目開發中,要求開發者使用預先編寫的智能指針類對象代替C語言中的原生指針
3 智能指針思想
工程中的智能指針是一個類模板
通過構造函數接管申請的內存
通過析構函數確保堆內存被及時釋放
通過重載指針運算符* 和 -> 來模擬指針的行為
通過重載比較運算符 == 和 != 來模擬指針的比較
class Test
{
public:
Test()
{
this->a = 10;
}
void printT()
{
cout<
}
private:
int a;
};
class MyTestPointer
{
public:
public:
MyTestPointer()
{
p = NULL;
}
MyTestPointer(Test* p)
{
this->p = p;
}
~MyTestPointer()
{
delete p;
}
Test* operator->()
{
return p;
}
Test& operator*()
{
return *p;
}
protected:
Test *p;
};
void main01_classp()
{
Test *p = new Test;
p->printT();
delete p;
MyTestPointer myp = new Test; //構造函數
myp->printT(); //重載操作符 ->
};
class MyIntPointer
{
public:
public:
MyIntPointer()
{
p = NULL;
}
MyIntPointer(int* p)
{
this->p = p;
}
~MyIntPointer()
{
delete p;
}
int* operator->()
{
return p;
}
int& operator*()
{
return *p;
}
protected:
int *p;
};
void main02_intp()
{
int *p = new int(100);
cout<<*p<
delete p;
MyIntPointer myp = new int(200);
cout<<*myp<
};
總結
操作符重載是C++的強大特性之一
操作符重載的本質是通過函數擴展操作符的語義
operator關鍵字是操作符重載的關鍵
friend關鍵字可以對函數或類開發訪問權限
操作符重載遵循函數重載的規則
操作符重載可以直接使用類的成員函數實現
=, [], ()和->操作符只能通過成員函數進行重載
++操作符通過一個int參數進行前置與後置的重載
C++中不要重載&&和||操作符
面向對象程序設計有4個主要特點:抽象、封裝、繼承和多態性。我們已經講解了類和對象,了解了面向對象程序設計的兩個重要特征一數據抽象與封裝,已經能夠設計出基於對象的程序,這是面向對象程序設計的基礎。
要較好地進行面向對象程序設計,還必須了解面向對象程序設計另外兩個重要特 征——繼承性和多態性。本章主要介紹有關繼承的知識,多態性將在後續章節中講解。
繼承性是面向對象程序設計最重要的特征,可以說,如果沒有掌握繼承性,就等於沒有掌握類和對象的精華,就是沒有掌握面向對象程序設計的真谛。
has-A,uses-A 和 is-A
has-A 包含關系,用以描述一個類由多個“部件類”構成。實現has-A關系用類成員表示,即一個類中的數據成員是另一種已經定義的類。
uses-A 一個類部分地使用另一個類。通過類之間成員函數的相互聯系,定義友員或對象參數傳遞實現。
is-A 機制稱為“繼承”。關系具有傳遞性,不具有對稱性。
萬事萬物中皆有繼承,是重要的現象
兩個案例:1)植物繼承圖;2)程序員繼承圖
注意:C++中的繼承方式(public、private、protected)會影響子類的對外訪問屬性。
1、子類擁有父類的所有成員變量和成員函數
2、子類可以擁有父類沒有的方法和屬性
3、子類就是一種特殊的父類
4、子類對象可以當作父類對象使用
派生類繼承了基類的全部成員變量和成員方法(除了構造和析構之外的成員方法),但是這些成員的訪問屬性,在派生過程中是可以調整的。
1、類成員訪問級別(public、private、protected)
2、思考:類成員的訪問級別只有public和private是否足夠?
1)C++中的繼承方式會影響子類的對外訪問屬性
public繼承:父類成員在子類中保持原有訪問級別
private繼承:父類成員在子類中變為private成員
protected繼承:父類中public成員會變成protected
父類中protected成員仍然為protected
父類中private成員仍然為private
2)private成員在子類中依然存在,但是卻無法訪問到。不論種方式繼承基類,派生類都不能直接使用基類的私有成員。
3)C++中子類對外訪問屬性表
父類成員訪問級別
繼
承
方
式
public
proteced
private
public
public
proteced
private
proteced
proteced
proteced
private
private
private
private
Private
4)繼承中的訪問控制
C++中的繼承方式(public、private、protected)會影響子類的對外訪問屬性
判斷某一句話,能否被訪問
1)看調用語句,這句話寫在子類的內部、外部
2)看子類如何從父類繼承(public、private、protected)
3)看父類中的訪問級別(public、private、protected)
思考:如何恰當的使用public,protected和private為成員聲明訪問級別?
1、需要被外界訪問的成員直接設置為public
2、只能在當前類中訪問的成員設置為private
3、只能在當前類和子類中訪問的成員設置為protected,protected成員的訪問權限介於public和private之間。
練習:
public繼承不會改變父類對外訪問屬性;
private繼承會改變父類對外訪問屬性為private;
protected繼承會部分改變父類對外訪問屬性。
結論:一般情況下class B : public A
//類的繼承方式對子類對外訪問屬性影響
#include
#include
using namespace std;
class A
{
private:
int a;
protected:
int b;
public:
int c;
A()
{
a = 0;
b = 0;
c = 0;
}
void set(int a, int b, int c)
{
this->a = a;
this->b = b;
this->c = c;
}
};
class B : public A
{
public:
void print()
{
//cout<<"a = "<
cout<<"b = "<
cout<<"c = "<
}
};
class C : protected A
{
public:
void print()
{
//cout<<"a = "<
cout<<"b = "<
cout<<"c = "<
}
};
class D : private A
{
public:
void print()
{
//cout<<"a = "<
cout<<"b = "<
cout<<"c = "<
}
};
int main_01(int argc, char *argv[])
{
A aa;
B bb;
C cc;
D dd;
aa.c = 100; //ok
bb.c = 100; //ok
//cc.c = 100; //err 類的外部是什麼含義
//dd.c = 100; //err
aa.set(1, 2, 3);
bb.set(10, 20, 30);
//cc.set(40, 50, 60); //ee
//dd.set(70, 80, 90); //ee
bb.print();
cc.print();
dd.print();
system("pause");
return 0;
}
類型兼容規則是指在需要基類對象的任何地方,都可以使用公有派生類的對象來替代。通過公有繼承,派生類得到了基類中除構造函數、析構函數之外的所有成員。這樣,公有派生類實際就具備了基類的所有功能,凡是基類能解決的問題,公有派生類都可以解決。類型兼容規則中所指的替代包括以下情況:
子類對象可以當作父類對象使用
子類對象可以直接賦值給父類對象
子類對象可以直接初始化父類對象
父類指針可以直接指向子類對象
父類引用可以直接引用子類對象
在替代之後,派生類對象就可以作為基類的對象使用,但是只能使用從基類繼承的成員。
類型兼容規則是多態性的重要基礎之一。
總結:子類就是特殊的父類 (base *p = &child;)
#include
#include
using namespace std;
/*
子類對象可以當作父類對象使用
子類對象可以直接賦值給父類對象
子類對象可以直接初始化父類對象
父類指針可以直接指向子類對象
父類引用可以直接引用子類對象
*/
//子類就是特殊的父類
class Parent03
{
protected:
const char* name;
public:
Parent03()
{
name = "Parent03";
}
void print()
{
cout<<"Name: "<
}
};
class Child03 : public Parent03
{
protected:
int i;
public:
Child03(int i)
{
this->name = "Child2";
this->i = i;
}
};
int main()
{
Child03 child03(1000);
//分別定義父類對象 父類指針 父類引用 child
Parent03 parent = child03;
Parent03* pp = &child03;
Parent03& rp = child03;
parent.print();
pp->print();
rp.print();
system("pause");
return 0;
}
類在C++編譯器的內部可以理解為結構體
子類是由父類成員疊加子類新成員得到的
繼承中構造和析構
問題:如何初始化父類成員?父類與子類的構造函數有什麼關系
在子類對象構造時,需要調用父類構造函數對其繼承得來的成員進行初始化
在子類對象析構時,需要調用父類析構函數對其繼承得來的成員進行清理
#include
#include
using namespace std;
class Parent04
{
public:
Parent04(const char* s)
{
cout<<"Parent04()"<<" "<
}
~Parent04()
{
cout<<"~Parent04()"<
}
};
class Child04 : public Parent04
{
public:
Child04() : Parent04("Parameter from Child!")
{
cout<<"Child04()"<
}
~Child04()
{
cout<<"~Child04()"<
}
};
void run04()
{
Child04 child;
}
int main_04(int argc, char *argv[])
{
run04();
system("pause");
return 0;
}
1、子類對象在創建時會首先調用父類的構造函數
2、父類構造函數執行結束後,執行子類的構造函數
3、當父類的構造函數有參數時,需要在子類的初始化列表中顯示調用
4、析構函數調用的先後順序與構造函數相反
原則: 先構造父類,再構造成員變量、最後構造自己
先析構自己,在析構成員變量、最後析構父類
//先構造的對象,後釋放
練習:demo05_extend_construct_destory.cpp
//子類對象如何初始化父類成員
//繼承中的構造和析構
//繼承和組合混搭情況下,構造函數、析構函數調用順序研究
#include
using namespace std;
class Object
{
public:
Object(const char* s)
{
cout<<"Object()"<<" "<
}
~Object()
{
cout<<"~Object()"<
}
};
class Parent : public Object
{
public:
Parent(const char* s) : Object(s)
{
cout<<"Parent()"<<" "<
}
~Parent()
{
cout<<"~Parent()"<
}
};
class Child : public Parent
{
protected:
Object o1;
Object o2;
public:
Child() : o2("o2"), o1("o1"), Parent("Parameter from Child!")
{
cout<<"Child()"<
}
~Child()
{
cout<<"~Child()"<
}
};
void run05()
{
Child child;
}
int main05(int argc, char *argv[])
{
cout<<"demo05_extend_construct_destory.cpp"<
run05();
system("pause");
return 0;
}
1、當子類成員變量與父類成員變量同名時
2、子類依然從父類繼承同名成員
3、在子類中通過作用域分辨符::進行同名成員區分(在派生類中使用基類的同名成員,顯式地使用類名限定符)
4、同名成員存儲在內存中的不同位置
總結:同名成員變量和成員函數通過作用域分辨符進行區分
繼承和static關鍵字在一起會產生什麼現象哪?
理論知識
? 基類定義的靜態成員,將被所有派生類共享
? 根據靜態成員自身的訪問特性和派生類的繼承方式,在類層次體系中具有不同的訪問性質 (遵守派生類的訪問控制)
? 派生類中訪問靜態成員,用以下形式顯式說明:
類名 :: 成員
或通過對象訪問 對象名 . 成員
總結:
1> static函數也遵守3個訪問原則
2> static易犯錯誤(不但要初始化,更重要的顯示的告訴編譯器分配內存)
3> 構造函數默認為private
多繼承概念
? 一個類有多個直接基類的繼承關系稱為多繼承
? 多繼承聲明語法
class 派生類名 : 訪問控制基類名1 , 訪問控制基類名2 , … , 訪問控制基類名n
{
數據成員和成員函數聲明
};
? 類 C 可以根據訪問控制同時繼承類 A 和類B 的成員,並添加
自己的成員
多繼承的派生類構造和訪問
? 多個基類的派生類構造函數可以用初始式調用基類構造函數初始化數據成員
? 執行順序與單繼承構造函數情況類似。多個直接基類構造函數執行順序取決於定義派生類時指定的各個繼承基類的順序。
? 一個派生類對象擁有多個直接或間接基類的成員。不同名成員訪問不會出現二義性。如果不同的基類有同名成員,派生類對象訪問時應該加以識別。
多繼承簡單應用
如果一個派生類從多個基類派生,而這些基類又有一個共同的基類,則在對該基類中聲明的名字進行訪問時,可能產生二義性
分析:
總結:
? 如果一個派生類從多個基類派生,而這些基類又有一個共同
的基類,則在對該基類中聲明的名字進行訪問時,可能產生
二義性
? 如果在多條繼承路徑上有一個公共的基類,那麼在繼承路徑的某處
匯合點,這個公共基類就會在派生類的對象中產生多個基類子對象
? 要使這個公共基類在派生類中只產生一個子對象,必須對這個基類
聲明為虛繼承,使這個基類成為虛基類。
? 虛繼承聲明使用關鍵字 virtual
實驗:注意增加virtual關鍵字後,構造函數調用的次數。
? 繼承是面向對象程序設計實現軟件重用的重要方法。程序員可以在已有基類的基礎上定義新的派生類。
? 單繼承的派生類只有一個基類。多繼承的派生類有多個基類。
? 派生類對基類成員的訪問由繼承方式和成員性質決定。
? 創建派生類對象時,先調用基類構造函數初始化派生類中的基類成員。調用析構函數的次序和調用構造函數的次序相反。
? C++提供虛繼承機制,防止類繼承關系中成員訪問的二義性。
? 多繼承提供了軟件重用的強大功能,也增加了程序的復雜性。
問題引出(賦值兼容性原則遇上函數重寫)
面向對象新需求
C++提供的多態解決方案
多態案例
多態工程意義
面向對象三大概念、三種境界(封裝、繼承、多態)
多態成立條件
總結條件、看代碼的時候要看出多態
如果子類定義了與父類中原型相同的函數會發生什麼?
函數重寫
在子類中定義與父類中原型相同的函數
函數重寫只發生在父類與子類之間
class Parent
{
public:
void print()
{
cout<<"Parent:print() do..."<
}
};
class Child : public Parent
{
public:
void print()
{
cout<<"Child:print() do..."<
}
};
int main01()
{
run00();
/*
Child child;
Parent *p = NULL;
p = &child;
child.print();
child.Parent::print();
*/
system("pause");
return 0;
}
父類中被重寫的函數依然會繼承給子類
默認情況下子類中重寫的函數將隱藏父類中的函數
通過作用域分辨符::可以訪問到父類中被隱藏的函數
/*
C/C++是靜態編譯型語言
在編譯時,編譯器自動根據指針的類型判斷指向的是一個什麼樣的對象
*/
/*
1、在編譯此函數的時,編譯器不可能知道指針 p 究竟指向了什麼。
2、編譯器沒有理由報錯。
3、於是,編譯器認為最安全的做法是編譯到父類的print函數,因為父類和子類肯定都有相同的print函數。
*/
//面向對象新需求
//如果我傳一個父類對象,執行父類的print函數
//如果我傳一個子類對象,執行子類的printf函數
//現象產生的原因
//賦值兼容性原則遇上函數重寫 出現的一個現象
//1 沒有理由報錯
//2 對被調用函數來講,在編譯器編譯期間,我就確定了,這個函數的參數是p,是Parent類型的。。。
//3靜態鏈編
//工程開發中如何判斷是不是多態存在?
/*
在同一個類裡面能實現函數重載
繼承的情況下,發生重寫
重載不一定;
重寫的定義
靜態聯編 重載是
動態聯編
*/
#include
using namespace std;
class Parent
{
public:
void print()
{
cout<<"Parent:print() do..."<
}
};
class Child : public Parent
{
public:
void print()
{
cout<<"Child:print() do..."<
}
};
/*
1、在編譯此函數的時,編譯器不可能知道指針 p 究竟指向了什麼。
2、編譯器沒有理由報錯。
3、於是,編譯器認為最安全的做法是編譯到父類的print函數,因為父類和子類肯定都有相同的print函數。
*/
void howToPrint(Parent* p)
{
p->print();
}
void run00()
{
Child child;
Parent* pp = &child;
Parent& rp = child;
//child.print();
//通過指針
//pp->print();
//通過引用
//rp.print();
howToPrint(&child);
}
int main01()
{
run00();
/*
Child child;
Parent *p = NULL;
p = &child;
child.print();
child.Parent::print();
*/
system("pause");
return 0;
}
編譯器的做法不是我們期望的
根據實際的對象類型來判斷重寫函數的調用
如果父類指針指向的是父類對象則調用父類中定義的函數
如果父類指針指向的是子類對象則調用子類中定義的重寫函數
? C++中通過virtual關鍵字對多態進行支持
? 使用virtual聲明的函數被重寫後即可展現多態特性
案例場景:
英雄戰機HeroFighter , AdvHeroFighter 分別和敵機EnemyFighter 戰斗.
power()attack()
#include "iostream"
using namespace std;
class HeroFighter
{
public:
public:
virtual int ackPower()
{
return 10;
}
};
class AdvHeroFighter : public HeroFighter
{
public:
virtual int ackPower()
{
return HeroFighter::ackPower()*2;
}
};
class enemyFighter
{
public:
int destoryPower()
{
return 15;
}
};
//如果把這個結構放在動態庫裡面
//寫了一個框架,可以調用
//我的第3代戰機代碼出現的時間晚於框架出現的時間。。。。
//框架 有使用後來人 寫的代碼的能力。。。
//面向對象3大概念
/*
封裝
突破了C語言函數的概念。。
繼承
代碼復用 。。。。我復用原來寫好的代碼。。。
多態
多態可以使用未來。。。。。80年代寫了一個框架。。。。。。90人寫的代碼
多態是我們軟件行業追尋的一個目標。。。
////
*/
//
void objPK(HeroFighter *hf, enemyFighter *enemyF)
{
if (hf->ackPower() >enemyF->destoryPower())
{
printf("英雄打敗敵人。。。勝利\n");
}
else
{
printf("英雄。。。犧牲\n");
}
}
void main()
{
HeroFighter hf;
enemyFighter ef;
objPK(&hf, &ef);
AdvHeroFighter advhf;
objPK(&advhf, &ef);
system("pause");
}
//面向對象3大概念
/*
封裝
突破了C語言函數的概念。。
繼承
代碼復用 。。。。我復用原來寫好的代碼。。。
多態
多態可以使用未來。。。。。80年代寫了一個框架。。。。。。90人寫的代碼
多態是我們軟件行業追尋的一個目標。。。
//寫了一個框架,可以調用後來人,寫的代碼的能力
////
*/
//間接賦值成立的3個條件
//1 定義兩個變量。。。
//2 建立關聯 。。。。
//3 *p
//多態成立的三個條件
//1 要有繼承
//2 要有函數重寫。。。C 虛函數
//3 要有父類指針(父類引用)指向子類對象
//多態是設計模式的基礎,多態是框架的基礎
01靜態聯編和動態聯編
1、聯編是指一個程序模塊、代碼之間互相關聯的過程。
2、靜態聯編(static binding),是程序的匹配、連接在編譯階段實現,也稱為早期匹配。
重載函數使用靜態聯編。
3、動態聯編是指程序聯編推遲到運行時進行,所以又稱為晚期聯編(遲綁定)。
switch 語句和 if 語句是動態聯編的例子。
4、理論聯系實際
1、C++與C相同,是靜態編譯型語言
2、在編譯時,編譯器自動根據指針的類型判斷指向的是一個什麼樣的對象;所以編譯器認為父類指針指向的是父類對象。
3、由於程序沒有運行,所以不可能知道父類指針指向的具體是父類對象還是子類對象
從程序安全的角度,編譯器假設父類指針只指向父類對象,因此編譯的結果為調用父類的成員函數。這種特性就是靜態聯編。
多態的實現效果
多態:同樣的調用語句有多種不同的表現形態;
多態實現的三個條件
有繼承、有virtual重寫、有父類指針(引用)指向子類對象。
多態的C++實現
virtual關鍵字,告訴編譯器這個函數要支持多態;不是根據指針類型判斷如何調用;而是要根據指針所指向的實際對象類型來判斷如何調用
多態的理論基礎
動態聯編PK靜態聯編。根據實際的對象類型來判斷重寫函數的調用。
多態的重要意義
設計模式的基礎 是框架的基石。
實現多態的理論基礎
函數指針做函數參數
C函數指針是C++至高無上的榮耀。C函數指針一般有兩種用法(正、反)。
多態原理探究
與面試官展開討論
c++編譯器多態實現原理
函數重載
必須在同一個類中進行
子類無法重載父類的函數,父類同名函數將被名稱覆蓋
重載是在編譯期間根據參數類型和個數決定函數調用
函數重寫
必須發生於父類與子類之間
並且父類與子類中的函數必須有完全相同的原型
使用virtual聲明之後能夠產生多態(如果不使用virtual,那叫重定義)
多態是在運行期間根據具體對象的類型決定函數調用
#include
#include
using namespace std;
class Parent01
{
public:
Parent01()
{
cout<<"Parent01:printf()..do"<
}
public:
virtual void func()
{
cout<<"Parent01:void func()"<
}
virtual void func(int i)
{
cout<<"Parent:void func(int i)"<
}
virtual void func(int i, int j)
{
cout<<"Parent:void func(int i, int j)"<
}
};
class Child01 : public Parent01
{
public:
//此處2個參數,和子類func函數是什麼關系
void func(int i, int j)
{
cout<<"Child:void func(int i, int j)"<<" "<
}
//此處3個參數的,和子類func函數是什麼關系
void func(int i, int j, int k)
{
cout<<"Child:void func(int i, int j, int k)"<<" "<
}
};
void run01(Parent01* p)
{
p->func(1, 2);
}
int main()
{
Parent01 p;
p.func();
p.func(1);
p.func(1, 2);
Child01 c;
//c.func(); //問題1
c.Parent01::func();
c.func(1, 2);
run01(&p);
run01(&c);
system("pause");
return 0;
}
//問題1:child對象繼承父類對象的func,請問這句話能運行嗎?why
//c.func(); //因為名稱覆蓋,C++編譯器不會去父類中尋找0個參數的func函數,只會在子類中找func函數。
//1子類裡面的func無法重載父類裡面的func
//2當父類和子類有相同的函數名、變量名出現,發生名稱覆蓋(子類的函數名,覆蓋了父類的函數名。)
//3//c.Parent::func();
//問題2 子類的兩個func和父類裡的三個func函數是什麼關系?
c++編譯器多態實現原理
在什麼情況下應當聲明虛函數
? 構造函數不能是虛函數。建立一個派生類對象時,必須從類層次的根開始,沿著繼承路徑逐個調用基類的構造函數
? 析構函數可以是虛的。虛析構函數用於指引 delete 運算符正確析構動態對象
父類指針和子類指針的步長
1) 鐵律1:指針也只一種數據類型,C++類對象的指針p++/--,仍然可用。
2) 指針運算是按照指針所指的類型進行的。
p++《=》p=p+1 //p = (unsigned int)basep + sizeof(*p) 步長。
3) 結論:父類p++與子類p++步長不同;不要混搭,不要用父類指針++方式操作數組。
理論知識:
? 當類中聲明虛函數時,編譯器會在類中生成一個虛函數表
? 虛函數表是一個存儲類成員函數指針的數據結構
? 虛函數表是由編譯器自動生成與維護的
? virtual成員函數會被編譯器放入虛函數表中
? 當存在虛函數時,每個對象中都有一個指向虛函數表的指針(C++編譯器給父類對象、子類對象提前布局vptr指針;當進行howToPrint(Parent *base)函數是,C++編譯器不需要區分子類對象或者父類對象,只需要再base指針中,找vptr指針即可。)
? VPTR一般作為類對象的第一個成員
C++中多態的實現原理
當類中聲明虛函數時,編譯器會在類中生成一個虛函數表
虛函數表是一個存儲類成員函數指針的數據結構
虛函數表是由編譯器自動生成與維護的
virtual成員函數會被編譯器放入虛函數表中
存在虛函數時,每個對象中都有一個指向虛函數表的指針(vptr指針)
說明1:
通過虛函數表指針VPTR調用重寫函數是在程序運行時進行的,因此需要通過尋址操作才能確定真正應該調用的函數。而普通成員函數是在編譯時就確定了調用的函數。在效率上,虛函數的效率要低很多。
說明2:
出於效率考慮,沒有必要將所有成員函數都聲明為虛函數
說明3 :C++編譯器,執行HowToPrint函數,不需要區分是子類對象還是父類對象
#include
using namespace std;
class A
{
public:
void printf()
{
cout<<"aaa"<
}
protected:
private:
int a;
};
class B
{
public:
virtual void printf()
{
cout<<"aaa"<
}
protected:
private:
int a;
};
void main()
{
//加上virtual關鍵字 c++編譯器會增加一個指向虛函數表的指針 。。。
printf("sizeof(a):%d, sizeof(b):%d \n", sizeof(A), sizeof(B));
cout<<"hello..."<
system("pause");
return ;
}
1)對象中的VPTR指針什麼時候被初始化?
對象在創建的時,由編譯器對VPTR指針進行初始化
只有當對象的構造完全結束後VPTR的指向才最終確定
父類對象的VPTR指向父類虛函數表
子類對象的VPTR指向子類虛函數表
2)分析過程
畫圖分析
C++中沒有Java中的接口概念,抽象類可以模擬Java中的接口類。(接口和協議)
工程上的多繼承
被實際開發經驗拋棄的多繼承
工程開發中真正意義上的多繼承是幾乎不被使用的
多重繼承帶來的代碼復雜性遠多於其帶來的便利
多重繼承對代碼維護性上的影響是災難性的
在設計方法上,任何多繼承都可以用單繼承代替
多繼承中的二義性和多繼承不能解決的問題
C++中是否有Java中的接口概念?
絕大多數面向對象語言都不支持多繼承
絕大多數面向對象語言都支持接口的概念
C++中沒有接口的概念
C++中可以使用純虛函數實現接口
接口類中只有函數原型定義,沒有任何數據的定義。
class Interface
{
public:
virtual void func1() = 0;
virtual void func2(int i) = 0;
virtual void func3(int i) = 0;
};
實際工程經驗證明
多重繼承接口不會帶來二義性和復雜性等問題
多重繼承可以通過精心設計用單繼承和接口來代替
接口類只是一個功能說明,而不是功能實現。
子類需要根據功能說明定義功能實現。
#include "iostream"
using namespace std;
/*
C++中沒有接口的概念
C++中可以使用純虛函數實現接口
接口類中只有函數原型定義,沒有任何數據的定義。
*/
class Interface1
{
public:
virtual void print() = 0;
virtual int add(int a, int b) = 0;
};
class Interface2
{
public:
virtual void print() = 0;
virtual int add(int a, int b) = 0;
virtual int minus(int a, int b) = 0;
};
class parent
{
public:
int a;
};
class Child : public parent, public Interface1, public Interface2
{
public:
void print()
{
cout<<"Child::print"<
}
int add(int a, int b)
{
return a + b;
}
int minus(int a, int b)
{
return a - b;
}
};
int main()
{
Child c;
c.print();
cout<
cout<
Interface1* i1 = &c;
Interface2* i2 = &c;
cout<
cout<
system("pause");
}
/*
編寫一個C++程序, 計算程序員( programmer )工資
1要求能計算出初級程序員( junior_programmer ) 中級程序員 ( mid_programmer )高級程序員( adv_programmer)的工資
2要求利用抽象類統一界面,方便程序的擴展, 比如:新增, 計算架構師 (architect ) 的工資
*/
理論知識
? 虛函數和多態性使成員函數根據調用對象的類型產生不同的動作
? 多態性特別適合於實現分層結構的軟件系統,便於對問題抽象時 定義共性,實現時定義區別
? 面向抽象類編程(面向接口編程)是項目開發中重要技能之一。
企業信息系統框架集成第三方產品
案例背景:一般的企業信息系統都有成熟的框架。軟件框架一般不發生變化,能自由的集成第三方廠商的產品。
案例需求:請你在企業信息系統框架中集成第三方廠商的Socket通信產品和第三方廠商加密產品。
第三方廠商的Socket通信產品:完成兩點之間的通信;
第三方廠商加密產品:完成數據發送時加密;數據解密時解密。
案例要求: 1)能支持多個廠商的Socket通信產品入圍
2)能支持多個第三方廠商加密產品的入圍
3)企業信息系統框架不輕易發生框架
需求實現
思考1:企業信息系統框架、第三方產品如何分層
思考2:企業信息系統框架,如何自由集成第三方產品
(軟件設計:模塊要求松、接口要求緊)
思考3:軟件分成以後,開發企業信息系統框架的程序員,應該做什麼?
第三方產品入圍應該做什麼?
編碼實現
分析有多少個類 CSocketProtocol CSckFactoryImp1 CSckFactoryImp2
CEncDesProtocol HwEncdes ciscoEncdes
1、定義CSocketProtocol 抽象類
2、編寫框架函數
3、編寫框架測試函數
4、廠商1(CSckFactoryImp1)實現CSocketProtocol、廠商2(CSckFactoryImp1)實現CSocketProtocol
5、抽象加密接口(CEncDesProtocol)、加密廠商1(CHwImp)、加密廠商2(CCiscoImp)),集成實現業務模型
6、框架(c語言函數方式,框架函數;c++類方式,框架類)
幾個重要的面向對象思想
繼承-組合(強弱)
注入
控制反轉 IOC
MVC
面向對象思想擴展aop思想
aop思想是對繼承編程思想的有力的補充
友情提示:今天課程內容,更加貼近實戰,並且語法和軟件思想都較難,請學員緊跟思路。課後加強復習!
結論: 只要你動手,又很容易!
函數三要素: 名稱、參數、返回值
C語言中的函數有自己特定的類型
C語言中通過typedef為函數類型重命名
typedef type name(parameter list)
typedef int f(int, int);
typedef void p(int);
函數指針
函數指針用於指向一個函數
函數名是函數體的入口地址
1)可通過函數類型定義函數指針: FuncType* pointer;
2)也可以直接定義:type (*pointer)(parameter list);
pointer為函數指針變量名
type為指向函數的返回值類型
parameter list為指向函數的參數類型列表
函數指針語法梳理
//函數類型
//函數指針類型
//函數指針變量
數組指針語法梳理
//數組類型語法
//數組指針類型
//數組指針變量
typedef int(FUNC)(int);
int test(int i)
{
return i * i;
}
void f()
{
printf("Call f()...\n");
}
int main()
{
FUNC* pt = test;
void(*pf)() = &f;
pf();
(*pf)();
printf("Function pointer call: %d\n", pt(3));
}
1、 指針做函數參數pk函數指針做函數參數
回憶指針做函數參數
一級指針做函數參數、二級。。。。、三級
2、 函數指針做函數參數
當函數指針 做為函數的參數,傳遞給一個被調用函數,
被調用函數就可以通過這個指針調用外部的函數,這就形成了回調
3、練習
int add(int a, int b)
int libfun( int (*pDis)(int a, int b) );
int main(void)
{
int (*pfun)(int a, int b);
pfun = add;
libfun(pfun);
}
int add(int a, int b)
{
return a + b;
}
int libfun( int (*pDis)(int a, int b) )
{
int a, b;
a = 1;
b = 2;
add(1,3) //直接調用add函數
printf("%d", pDis(a, b)); //通過函數指針做函數參數,間接調用add函數
//思考 這樣寫 pDis(a, b)有什麼好處?
}
//剖析思路
//1函數的調用 和 函數的實現 有效的分離
//2 C++的多態,可擴展
現在這幾個函數是在同一個文件當中
假如
int libfun(int (*pDis)(int a, int b))
是一個庫中的函數,就只有使用回調了,通過函數指針參數將外部函數地址傳入
來實現調用
函數 add 的代碼作了修改,也不必改動庫的代碼,就可以正常實現調用
便於程序的維護和升級
回調函數思想:
結論:回調函數的本質:提前做了一個協議的約定(把函數的參數、函數返回值提前約定)
請思考:C編譯器通過那個具體的語法,實現解耦合的?
C++編譯器通過多態的機制(提前布局vptr指針和虛函數表,找虛函數入口地址來實現)
1、 函數指針做函數參數,調用方式
被調用函數和主調函數在同一文件中(用來教學,沒有任何意義)
2、函數指針做函數參數
被調用函數和主調函數不在同一個文件中、模塊中。
難點:理解被調用函數是什麼機制被調用起來的。框架
框架提前設置了被調用函數的入口(框架提供了第三方模塊入口地址的集成功能)
框架具備調用第三方模塊入口函數
3、 練習
typedef int (*EncDataFunc)(unsigned char *inData,int inDataLen,unsigned char *outData,int *outDataLen,void *Ref, int RefLen);
int MyEncDataFunc(unsigned char *inData,int inDataLen,unsigned char *outData,int *outDataLen,void *Ref, int RefLen)
{
int rv = 0;
char *p = "222222222222";
strcpy(outData, p);
*outDataLen = strlen(p);
return rv;
}
int Send_Data(EncDataFunc encDataFunc, unsigned char *inData, int inDataLen, unsigned char *outData, int *outDatalen)
{
int rv = 0;
if (encDataFunc != NULL)
{
rv = encDataFunc(inData, inDataLen, outData, outDatalen, NULL, 0);
if (rv != 0)
{
printf("func encDataFunc() err.\n");
return rv;
}
}
return rv;
}
int main()
{
int rv = 0;
EncDataFunc encDataFunc = NULL;
encDataFunc = MyEncDataFunc;
// 第一個調用
{
unsigned char inData[2048];
int inDataLen;
unsigned char outData[2048];
int outDatalen;
strcpy(inData, "1111");
inDataLen = strlen(inData);
rv = encDataFunc(inData,inDataLen, outData, &outDatalen, NULL, 0);
if (rv != 0)
{
printf("edf err .....\n");
}
else
{
printf("edf ok \n");
printf("%s \n", outData);
}
}
{
unsigned char inData[2048];
int inDataLen;
unsigned char outData[2048];
int outDatalen;
strcpy(inData, "3333");
inDataLen = strlen(inData);
rv = Send_Data(MyEncDataFunc, inData, inDataLen, outData, &outDatalen);
if (rv != 0)
{
printf("func Send_Data err:%d", rv);
return rv;
}
printf("%s \n", outData);
}
getchar();
}
回調函數效果展示。
C語言版本Socket動態庫升級成框架集成第三方產品
簡稱:C動態庫升級成框架案例
名字解釋
動態庫:抽象類一個套接口,單獨封裝成模塊,供別人調用;無法擴展。
框架:能自由的擴展
案例背景:一般的企業信息系統都有成熟的框架,可以有C語言寫,也可以由C++語言。軟件框架一般不發生變化,能自由的集成第三方廠商的產品。
案例需求:在socket通信庫中,完成數據加密功能,有n個廠商的加密產品供你選擇,如何實現動態庫和第三個廠商產品的解耦合。
提醒:C++通過抽象類,也就是面向抽象類編程實現的(相當於C++編譯器通過多態機制,已經很好用了。提前布局vptr指針、虛函數表;調用是遲綁定完成。),
C語言中如何實現哪?
案例要求: 1)能支持多個第三方廠商加密產品的入圍
2)企業信息系統框架不輕易發生框架
需求實現思路分析
思考1:企業信息系統框架、第三方產品如何分層
思考2:企業信息系統框架,如何自由集成第三方產品
(軟件設計:模塊要求松、接口要求緊)
思考3:軟件分層確定後,動態庫應該做什麼?產品入圍廠商應該做什麼?
以後,開發企業信息系統框架的程序員,應該做什麼?
第三方產品入圍應該做什麼?
編碼實現
1、動態庫中定義協議,並完成任務的調用
typedef int (*EncData)(unsigned char *inData,int inDataLen,unsigned char*outData,int *outDataLen,void *Ref, int RefLen);
typedef int (*DecData)(unsigned char *inData,int inDataLen,unsigned char*outData,int *outDataLen,void *Ref, int RefLen);
2、加密廠商完成協議函數的編寫
3、對接調試。
4、動態庫中可以緩存第三方函數的入口地址,也可以不緩存,兩種實現方式。
案例總結
回調函數:利用函數指針做函數參數,實現的一種調用機制,具體任務的實現者,可以不知道什麼時候被調用。
回調機制原理:
當具體事件發生時,調用者通過函數指針調用具體函數
回調機制的將調用者和被調函數分開,兩者互不依賴
任務的實現 和 任務的調用 可以耦合 (提前進行接口的封裝和設計)
劉備利用周瑜、曹仁厮殺之際,乘虛襲取了南郡、荊州、襄陽,以後又征服了長沙等四郡。周瑜想想十分氣恨,正無處報復以奪還荊州。不久,劉備忽然喪偶,周瑜計上心來,對孫權說:“您的妹妹,美麗、剛強,我們以聯姻抗曹名義向劉備招親,把他騙來南徐幽禁,逼他們拿荊州來換。”孫權大喜,郎派人到荊州說親。
劉備認為這是騙局,想要拒絕,諸葛亮笑道:“送個好妻子上門何不答應?您只管去東吳,我叫趙雲陪您去,自有安排,包您得了夫人又不失荊州。”
接著,諸葛亮暗暗關照趙雲道:“我這裡有三個錦囊,內藏三條妙計。到南徐時打開第一個,到年底時打開第二個,危急無路時打開第三個。”
第一個錦囊
一到東吳就拜會喬國老
第二個錦囊
劉備被孫權設計留下就對他謊稱曹操大軍壓境
第三個錦囊
被東吳軍隊追趕就求孫夫人解圍