類的三大屬性:
private,public,protected
1,對于類的成員變量或者函數,預設即為私有
#include <iostream>
using namespace std;
class A
{
int y; //私有成員
int x; //私有成員
public:
A(int xx, int yy) {x = xx; y = yy;}
void setx(int m) {x = m;}
void sety(int n) {y = n;}
};
int main()
{
A a(1,2);
}
2,對于類的繼承而言,預設也是私有繼承!!!!!
#include <iostream>
using namespace std;
class A
{
int y;
int x;
public:
A(int xx, int yy) {x = xx; y = yy;}
void setx(int m) {x = m;}
void sety(int n) {y = n;}
};
class B:A
{
int x;
public:
B(int xx, int yy,int xxx):A(xx,yy)
{ x = xxx; }
};
int main()
{
B b(1,2,3);
}
如上,B也是同樣的私有繼承A了。。。。
3,内聯函數
内聯函數是一種特殊的函數,本身比較少,運作起來也特别快,,由于函數調用會産生一定的調用語句,參數的
傳遞,入棧出棧,那麼當這個函數被反複調用的時候,就會産生很大的開銷。于是就産生了内聯函數。
當我們的編譯器看到了關鍵字:inline的時候,那麼編譯器會把函數的實作全部插入到調用内聯函數出,這樣就
不會産生額外的調用開銷了
内聯成員函數:
1,inline + 成員函數
2,整個函數體出現在定義類的内部
#include <iostream>
using namespace std;
class A
{
int y;
int x;
public:
A(int xx, int yy) {x = xx; y = yy;}
void setx(int m) {x = m;} //為内聯函數,函數的定義在class中
inline void sety(int n); //同樣的為内聯函數,通過inline聲明
};
void A::sety(int n)
{
y = n;
}
int main()
{
A a(1,2);
}
内聯函數,從代碼層看,有函數的結構,而編譯後卻沒有函數的性質,不是在調用時候發生控制轉移,而是
在編譯的時候,将函數體嵌入到每一個調用之處。
3,成員函數的重載以及參數預設
我們知道函數是可以重載的,不同的傳回值類型或者函數的個數是不相同的,那麼就是函數的重載,同樣:成員
函數也是可以重載的。成員函數也是可以帶有預設的參數的。C++文法規定,函數在寫的時候可以有預設值,那麼
在調用的時候,如果我們沒有給參數,那麼參數就是預設值。
#include <iostream>
using namespace std;
class A
{
int y;
int x;
public:
void init(int x = 0, int y = 0); //預設參數
void setx(int m) {x = m;} //下面這兩個函數為setx函數的重載
int setx() {return x;}
};
int main()
{
A a(1,2);
}
這裡我們需要注意的就是使用:預設參數時和函數重載時的二異行,,,,,
如下:程式
#include <iostream>
using namespace std;
class A
{
int y;
int x;
public:
void init(int xx = 0, int yy = 0); //預設參數
void setx(int m =0) {x = m;}
int setx() {return x;}
};
int main()
{
A a(1,2);
a.setx();
}
如上的setx函數,a調用setx函數,就不知道調用的是哪一個,就會産生編譯出錯,,,,:
5,構造函數
一:成員函數的一種,名字與類名相同,可以有參數,不能有傳回值(任何類型都不行,包括void)
二:作用是對類對象進行初始化,如:給成員變量賦初值
三:如果定義類的時候沒有寫構造函數,則編譯器生成一個預設的無參構造函數(不做任何操作)
四:如果我們定義了構造函數,那麼編譯器不會生成預設的無參構造函數
五:對象生成時構造函數自動被調用(一定),對象一旦生成,就再也不能在其上執行構造函數
六:一個類中可以有多個構造函數,構造函數的重載,那麼如果要調用那個構造函數,需要看我們的對象是如何
調用的
那麼為什麼需要構造函數呢????
構造函數執行必要的初始化工作,有了構造函數就不必在專門寫必要的初始化函數了,也不用擔心忘記了寫
初始化函數了,有時候對象沒有被初始化就使用,那麼就會導緻編譯錯誤了。。。。
#include <iostream>
using namespace std;
class A
{
int y;
int x;
public:
void init(int xx = 0, int yy = 0); //預設參數
void setx(int m =0) {x = m;}
};
//編譯器自動的調用預設構造函數
int main()
{
A a; //預設的構造函數被調用
A *pc = new A; //預設的構造函數被調用
pc->init(1,2);
}
如上,隻要有對象生成,就一定會調用構造函數
構造函數的重載及其調用問題:
#include <iostream>
using namespace std;
class A
{
double y;
double x;
public:
A(double xx, double yy)
{ x = xx; y = yy;}
A(double xxx)
{ x = xxx;}
void setx(int m =0) {x = m;}
};
//構造函數的重載
int main()
{
A a(2,3); //調用的是第一個構造函數,整形被轉化成了double類型
A b(2); //調用的是第二個構造函數,同樣的被轉化成了double
}
構造函數在數組(對象數組)中的使用:
#include <iostream>
using namespace std;
class A
{
double x;
public:
A()
{ cout<<"Construction 1"<<endl; }
A(double xx)
{ cout<<"Construction 2"<<endl; x = xx;}
void setx(int m =0) {x = m;}
};
int main()
{
A a1[2]; //調用無參的構造函數
cout<<"step1"<<endl;
A a2[2] = {4,5}; //調用有參的構造函數初始化
cout<<"step2"<<endl;
A a3[2] = {3}; //一個調用有參的構造函數,一個調用無參的構造函數
A *p = new A[2]; //調用無參的構造函數
delete[] p;
}
運作結果:
如上:這裡還是有很多不同的,,,,,,生成一個對象就一定要調用構造函數
還有一種特殊的例子,我們來看一下:
#include <iostream>
using namespace std;
class A
{
double x;
double y;
public:
A() //無參構造函數
{ cout<<"Construction 1"<<endl; }
A(double xxx) //一個參數構造函數
{ cout<<"Construction 2"<<endl; x = xxx;}
A(double xx, double yy) //兩個參數構造函數
{ cout<<"Construction 3"<<endl; x = xx; y = yy;}
void setx(int m =0) {x = m;}
};
int main()
{
A a[3] = {1, A(2,3)}; //定義了一個對象數組,第一個一個元素初始化,第二個兩個元素,第三個沒有初始化
A *p[3] = {new A(3), new A, new A(4,5)};
}
運作結果:
從上面可以看出,我們的對象數組是這個樣子進行初始化的。。。。。。。。。。。并且也是這個樣子調用new的。
其實總的來說:
對于對象數組的初始化:嗯嗯,好像隻能用這種方式:
A a[3] = {A(1), A(2,3)};
A *p[3] = {new A(3), new A, new A(4,5)};
嗯嗯,就是,這種方式:上面我們實作的方式是不一樣的,也就是說:當隻有一個參數的時候
A(1)和1是一樣的,,,,,,,,,,,
即就是:
A a = {1},A a(1),A a = {A(1)}; //三者是一樣的!!!!!
6,複制構造函數(Copy Constructor)
我們經常使用函數,傳遞各種各樣的參數,而我們今天要說的複制構造函數:就是把對象(注意是對象,不是
對象的指針或者對象的引用)作為一個參數,并且傳遞給函數
我們也知道,把參數傳遞給函數有三種方法:值傳遞,位址傳遞,傳引用。
值傳遞時,傳送的是作為實際參數的對象的副本而不是實際對象本身。而且這個副本的内容是按位從原始參數
那裡複制過來的,内容是相同的。
當傳遞過來的時候,如果是類的對象呢???會不會觸發類的構造函數呢,,調用結束,會不會觸發析構呢???
從程式看:
#include <iostream>
using namespace std;
class A
{
int x;
public:
A(int x1)
{ x = x1; cout<<"Constructor"<<endl; } //構造函數的實作
~A()
{ cout<<"Destructor"<<endl; } //析構函數的實作
void print()
{ cout<<x<<endl; }
};
void f(A a)
{
a.print();
}
int main()
{
A a(10);
f(a);
a.print();
return 0;
}
運作結果:
從運作結果可以看到:A類的構造函數隻被調用了一次,發生在建立對象a的時候,而A類的析構函數卻被調用了兩次
這是為什麼呢???
查閱書籍後發現:如同我們上面所提到的,把一個對象作為參數傳遞給函數時,同時建立了該對象的副本(這
個副本将作為函數的參數)。也就是說:建立了一個新的對象。當函數結束時,作為函數的實際參數的副本将被
銷毀,這産生了兩個有趣的問題。。。。。。。
1,在建立對象的副本時有沒有調用構造函數
2,在銷毀對象的副本時有沒有調用了析構函數
首先,在調用函數時,程式建立了一個對象的副本作為形式參數,此時普通的構造函數并沒有被調用,而是調用
了複制構造函數,複制構造函數定義了如何建立了一個對象的副本。。。。。如果一個類中沒有顯示的地定義類
的複制構造函數,那麼C++将提供一個預設的複制構造函數。預設的複制構造函數将以按位複制的形式建立一個
對象的副本,即建立一個與原對象一樣的副本。
由于普通的構造函數通常用于初始化對象的某些成員,是以就不能調用普通的構造函數建立對象的副本,因為
這樣産生的對象可能與現有的對象的狀态屬性不完全相同。很簡單,隻是因為當把一個對象傳遞給函數時,需要
使用的是對象的目前狀态,而不是初始狀态。。。。
其次,當函數結束時,由于作為參數的對象副本超出了作用域,是以它将被銷毀,進而調用了析構函數,這就是
為什麼前面的程式中調用了兩次析構函數的原因,第一次函數f()參數超出了作用域,第二次是因為main函數
中的對象a在程式結束時被銷毀。。。。
綜上所述:當建立一個對象的副本作為函數參數時,普通的構造函數沒有被調用,所調用的構造函數是按位複制
的預設複制構造函數。但是,當對象的副本被銷毀時(通常因為,函數傳回而超出了作用域),析構函數被調用
,如果析構函數是空的話,通常不會發生什麼問題,但是一般情況下:析構函數都要完成一些清理工作(釋放
記憶體。。。。)
假如,我們真的在複制構造函數裡面為一個指針變量配置設定了記憶體,在析構函數裡釋放了配置設定給這個指針所指向的
空間,那麼我們可以想想,在把對象傳遞給函數到整個main函數傳回的時候,發生了什麼???
首先,有一個對象的副本産生了,這個副本也有一個指針,,,它和原始對象的指針指向同塊記憶體空間,,函數
傳回時,副本對象的析構函數被執行了,,,釋放了對象副本裡指針所執行的記憶體空間,但是這個記憶體空間對于
原始對象來說,還是有用的啊,,,,(幹嘛就直接給釋放了呢???),就程式本身而言,這是一個錯誤,
但是錯誤還是沒有停止,,,等到main函數結束的時候,原始對象的析構函數繼續被執行,,,對同一塊記憶體
釋放兩次,這有可能加深錯誤!!!
那麼,如何解決上面的問題呢???
嗯嗯,可以用傳位址,傳引用,這樣卻是可以合理的解決這樣一個問題,但是,有的時候,我們不需要直接的改
變外部變量啊,,,那如何處理呢????
其實,這個時候,我們可以用複制構造函數來解決這個問題,複制構造函數是在産生對象副本的時候執行的,
我們可以調用自己的複制構造函數。在自己的複制構造函數裡,我們可以申請一個新的記憶體空間來儲存那個
構造函數所指向的内容。。。。這樣,在執行對象副本的時候,執行的就是剛剛申請的那個空間了。。。
、
複制構造函數(拷貝構造函數)
X&
const X&
volatile X&
const volatile X&
//後面也可以跟其他的參數。。。。。。。。。。。。。
如上,就是複制構造函數的幾種類型。。。。。。。。。
1, 當一個對象副本被作為參數傳遞給函數時
display(y);
2, 當一個對象被另一個對象顯式的初始化時
my_string x = y;
3, 當建立一個臨時對象時(最常見的是:作為函數的傳回值)
y = func2();
前面2中情況,對象y的引用将被傳遞給複制構造函數;在第三種情況下,函數func2()傳回的對象引用将被傳遞給
y的複制構造函數。。。。
關于對象之間的顯示初始化。。。
#include <iostream>
using namespace std;
class A
{
int x;
public:
A(int x1)
{ x = x1; cout<<"Constructor"<<endl; } //構造函數的實作
~A()
{ cout<<"Destructor"<<endl; } //析構函數的實作
void print()
{ cout<<x<<endl; }
};
int main()
{
A a(10);
A b = a; // 可以這樣初始化
A c(a); // 也可以這樣初始化
return 0;
}
運作結果:
其實後面兩個的初始化調用的是複制構造函數,隻是在目前類中我們沒寫,調用的是預設,析構調用了 ,,,,
程式:
#include <iostream>
using namespace std;
class A
{
int x;
public:
A(int x1)
{ x = x1; cout<<"Constructor"<<endl; } //構造函數的實作
A(A & a) //自定義的複制構造函數
{ x = a.x; cout<<"自定義的 Constructor"<<endl;}
~A()
{ cout<<"Destructor"<<endl; } //析構函數的實作
void print()
{ cout<<x<<endl; }
};
int main()
{
A a(10);
A b = a;
A c(a);
return 0;
}
程式如上:我們寫了自定義的複制構造函數
運作結果:
為了明顯的區分開跟指派的差別,我們重載了一個指派運算符:
#include <iostream>
using namespace std;
class A
{
int x;
public:
A(int x1)
{ x = x1; cout<<"Constructor"<<endl; } //構造函數的實作
A(A & a) //自定義的複制構造函數
{ x = a.x; cout<<"自定義的 Constructor"<<endl;}
A &operator = (A & a) //重載了一個指派運算符
{ this->x = a.x; cout<<"指派運算符的重載"<<endl;}
~A()
{ cout<<"Destructor"<<endl; } //析構函數的實作
void print()
{ cout<<x<<endl; }
};
int main()
{
A a(10);
A b(2);
b = a;
return 0;
}
運作結果:
1,上面我們知道了一些關于複制構造函數裡面的一些東西。。。。那麼我們有一些疑惑???
複制構造函數可以重載嗎????
可以的,因為上面我們提到了,複制構造函數的類型;;;;;;;;
#include <iostream>
using namespace std;
class A
{
int x;
public:
A(int x1)
{ x = x1; cout<<"Constructor"<<endl; } //構造函數的實作
A(A & a) //自定義的複制構造函數
{ x = a.x; cout<<"自定義的 Constructor"<<endl;}
A(A & a, int b)
{ a.x = b; cout<<"重載的複制構造函數"<<endl; }
~A()
{ cout<<"Destructor"<<endl; } //析構函數的實作
void print()
{ cout<<x<<endl; }
};
int main()
{
A a(2);
A b(a, 3);
return 0;
}
如上:我們重載了複制構造函數....
運作結果:
2,拷貝構造函數中可以調用private成員變量嗎???
從上面的程式中可以看到,這裡的調用無壓力,,,,可以直接調用,,,因為是一種特殊的構造函數
操作的是自己的成員變量,不用考慮private。。。。。
關于深拷貝,淺拷貝
其實說白了:編譯器預設的拷貝構造函數其實就是一個淺拷貝,如同我們上面所提到的那樣,如果有指針變量的
話,那麼就會出錯,因為拷貝後兩個指針指向了同一塊空間,釋放兩次自然就會出錯。。。。而深拷貝,不但對指針
進行拷貝,也對指向的内容進行拷貝。。。。。。。拷貝後,是兩個指向不同位址的指針。。。。。。
1,那麼淺拷貝會出現什麼問題呢????
int main()
{
A a(2);
A b = a;
// A c(a); 上面這兩種情況都是可以的
return 0;
}
假設類有一個成員變量指針:char *p;
1,淺拷貝隻是拷貝了指針,使得兩個指針指向同一個位址,這樣在對象塊結束,調用函數析構的時,會造成同一
份資源析構2次,即delete同一塊記憶體2次,造成程式崩潰。
2,淺拷貝使得b.p和a.p指向同一塊記憶體,任何一方的變動都會影響到另一方
3,結果是會調用兩次析構函數,造成同一塊記憶體釋放兩次,明顯的錯誤
如下:
#include <string.h>
#include <iostream>
using namespace std;
class A
{
char *p;
public:
A(char *s) //構造函數
{
cout<<"Constructor"<<endl;
p = new char[strlen(s) + 1];
strcpy(p, s);
}
~A() //析構函數
{
if(p != NULL)
{
delete p;
p = NULL;
cout<<"析構函數"<<endl;
}
}
};
int main()
{
char p[] = "abc";
A a(p);
return 0;
}
運作結果:
一塊空間的多次釋放,自然而然的造成了錯誤。 ,,,,,,,,,,,,,,
深拷貝采用了在堆記憶體中申請新的空間來存儲資料,在我們重新書寫的複制構造函數裡我們重新為這個新的指針
配置設定了一塊存儲空間了,,,,,,,,,,
#include <string.h>
#include <iostream>
using namespace std;
class A
{
char *p;
public:
A(char *s) //構造函數
{
cout<<"Constructor"<<endl;
p = new char[strlen(s) + 1];
strcpy(p, s);
}
A(A & a)
{
cout<<"複制Constructor"<<endl;
int length = strlen(a.p);
p = new char[length + 1];
strcpy(p, a.p);
}
~A() //析構函數
{
if(p != NULL)
{
delete p;
p = NULL;
cout<<"析構函數"<<endl;
}
}
};
int main()
{
char p[] = "abc";
A a(p);
A b = a;
return 0;
}
可以看到:我們重新寫了複制構造函數,并且在其中重新配置設定了記憶體空間了。。。。
運作結果: