C++ Primer Plus讀書筆記
1.類靜態成員
類中可以有一些靜態變量,不管這個類創建了多少個對象,這個變量始終都是保持不變的,因爲它是靜態變量,這就給一些在所有對象中都有相同值的類提供了方便。
在進行類的變量定義時,將數組定義成指針的形式,可以不在定義時就將大小分配,而是在構造函數中使用new對其分配空間。
2.析構函數的調用
- 析構函數的調用時機:
- 當在成員函數中創建對象時,該函數執行完畢之後會調用析構函數!
- 當對象是動態變量時,執行完定義該對象的程序塊時,將調用該對象的析構函數。
- 當對象是靜態變量時,程序結束時將調用對象的析構函數。
- 當對象是用new創建的時,僅當顯示使用delete刪除對象時,析構函數纔會被調用。
3.特殊成員函數
C++自動提供的成員函數:
- 默認構造函數
- 默認的構造函數裏面是沒有內容的
- 默認析構函數
- 類似與默認構造函數,是沒有內容的
複製構造函數
用於將一個對象複製到新創建的對象中,它用於初始化過程中,而不是常規的賦值過程中。它的函數原型爲:
class_name(const class_name &) //ZhuMeng(const ZhuMeng &)
新建一個對象並將其初始化爲現有對象時,複製構造函數將被調用
默認的複製構造函數複製的是成員的值。
賦值運算符
- 這種運算符的原型爲:
class_name & class_name::operator=(const class_name &);
- 這種運算符的原型爲:
- 地址運算符
- 默認構造函數
4.這裏介紹一種錯誤
- 頭文件
#ifndef ZHUMENG_H_
#define ZHUMENG_H
#include<iostream>
class ZhuMeng
{
private:
int shi;
int fen;
int miao;
static int numm;
public:
ZhuMeng();
ZhuMeng(int , int , int );
explicit ZhuMeng(int );
~ZhuMeng();
ZhuMeng operator+(ZhuMeng &) const;
friend std::ostream & operator<<(std::ostream & o, const ZhuMeng & outp);
};
#endif
- 實現文件
#include"ZhuMeng.h"
int ZhuMeng::numm = 0;
ZhuMeng::ZhuMeng()
{
shi = 0;
fen = 0;
miao = 0;
numm++;
}
ZhuMeng::ZhuMeng(int shi, int fen, int miao)
{
this->shi = shi;
this->fen = fen;
this->miao = miao;
numm++;
}
ZhuMeng::ZhuMeng(int shit)
{
this->shi = shit;
numm++;
}
ZhuMeng::~ZhuMeng()
{
numm--;
}
ZhuMeng ZhuMeng::operator+(ZhuMeng & aaa) const
{
std::cout << this->shi << std::endl;
ZhuMeng sum;
sum.shi = this->shi + aaa.shi;
sum.fen = this->fen + aaa.fen;
sum.miao = this->miao + aaa.miao;
return sum;
}
std::ostream & operator<<(std::ostream & o, const ZhuMeng & outp)
{
o<<outp.shi<<std::endl<<outp.fen<<std::endl<<outp.miao<<std::endl;
o<<"left num: "<<outp.numm;
return o;
}
- 執行文件
#include<iostream>
#include"ZhuMeng.h"
using namespace std;
ZhuMeng operator*(int all, ZhuMeng all_2)
{
cout<<"success"<<endl;
}
void func(ZhuMeng a)
{
cout<<a<<endl;
}
int main()
{
ZhuMeng default_zhumeng;
ZhuMeng a1(13, 24, 10);
ZhuMeng a2;//=a1; **** #1
func(default_zhumeng);
func(a1);
func(default_zhumeng);
func(a1);
func(a1);
return 0;
}
輸出的結果爲:
0
0
0
left num: 2
13
24
10
left num: 1
0
0
0
left num: 0
13
24
10
left num: -1
13
24
10
left num: -2
下面從以下幾個角度來分析
這裏的numm在類中作爲靜態變量,初始值定義在cpp文件中,靜態變量在內存中一直存在,作爲某個類所有對象之間共享的一個變量。
numm的值爲什麼會變成負數呢?
在重載<<運算符的時候,將一個ZhuMeng的對象作爲對象傳給這個函數
函數的參數傳遞方式是按值傳遞,因此編譯器內部是將實參的值賦給形參,也就是用到了複製構造函數,這時候會調用實參的析構函數,以及賦值所用到的複製構造函數,並不是使用構造函數,而在C++中默認的賦值運算符是沒有讓numm++的,因此numm的值會-1。因此想要正確地執行所想的功能,這裏應該是定義一個複製構造函數
ZhuMeng(const ZhuMeng & s) { numm++; }
1部分的代碼有什麼問題呢?
這部分代碼,把等號加上,就是進行了賦值操作,而默認的賦值函數沒有記性numm++操作,同時這個賦值也是不安全的,如果有使用new delete而創建刪除的變量,則很有可能會出現錯誤,因爲指針和內存區域也進行了複製,而每次把對象當做實參進行傳遞之後會調用析構函數,會將前面使用new的變量刪除,同時後面如果使用兩次的話,會重複刪除同一塊區域導致意想不到的錯誤。
因此解決方法是顯式定義賦值運算符(也就是重載 = 運算符)
ZhuMeng & ZhuMeng::operator=(const ZhuMeng & a) { numm++; //這裏重新定義那些使用new分配空間的變量,並將a的對應值賦給他們即可 return *this; }
- 還有一個問題:如果在類的函數中,某些數據是使用new delete函數進行創建的話,則需要在複製構造函數和賦值運算符中創建新的內存區域。如果不這樣做,則每一次調用析構函數的時候,刪除的都是同一個內容(因爲默認的複製和賦值函數都是這樣處理的),當一個變量被delete兩次的時候,就會發生未知的錯誤!!切記。
5.構造函數中使用new時應該注意的事項
如果在構造函數中使用new來初始化指針成員,則需要在析構函數中使用delete
new和delete必須相互兼容,new對應delete,new[]對應delete[]
- 如果有多個構造函數,則必須以同樣的方式使用new,要麼都帶[],要麼都不帶,因爲只有一個析構函數,所有的構造函數都必須與他兼容。
- 可以在構造函數中將指針初始化爲0(C++11爲nullptr),因爲delete和delete[]都可以用於空指針。
- 應當定義一個複製構造函數,通過深度複製將一個對象初始化爲另一個對象。
- 應當定義一個賦值運算符,通過深度複製的方式將一個對象複製給另一個對象。
- 下面是幾個錯誤的例子
String String()
{
str = "123";//沒有使用new
}
String String(const char * s)
{
str = new char;//因爲是字符串,沒有使用[]是錯誤的
}
String String(const char * s)
{
len = strlen(s);
str = new char[len+1];//這個是正確的,在長度後面留一個\0的位置
}
5.關於返回對象
1 返回指向const對象的引用
使用const引用的常見原因是旨在提高效率,但對於何時可以採用這種方式存在一些限制。如果函數返回(通過調用對象的方法或將對象作爲參數)傳遞給它的對象,可以通過返回引用來提高效率。
注意:
- 返回對象將調用複製構造函數,而返回引用則不會
- 引用指向的對象應該在調用函數執行時存在
2 返回指向非const對象的引用
重載賦值運算符
- 這個是爲了提高效率
重載<<運算符(與cout一起使用)
- 這個是必須這樣做
3 返回對象
- 如果返回的對象是被調用函數中的局部變量,則不應該按引用的方式返回它,因爲在被調用函數執行完畢時,局部對象將執行其析構函數,因此,當控制權回到調用函數時,引用指向的對象將不再存在。
4 返回const對象
- 主要是爲了防止某些函數的左值被用於計算,比如a+b=c,將c賦給加法的結果,當然這樣是沒有意義的,如果返回const對象則可能會避免發生這種錯誤。
總結:如果方法或函數要返回局部對象,則應該返回對象,而不是指向對象的引用。在這種情況下,將使用複製構造函數來生成返回的對象。如果方法或函數要返回一個沒有公有複製構造函數的類的對象,則必須返回一個指向這種對象的引用。如果返回引用和返回對象都可以的時候則最好使用返回引用的方法,效率會更高。
6.指向對象的指針
- 使用new獲取指向對象的指針,使用delete則會調用對象的析構函數
ZhuMeng * pointer = new ZhuMeng();//括號裏針對不同構造函數有不同的寫法
- 定位new運算符
char * buffer = new char[512];
ZhuMeng *pc3, *pc4;
pc3 = new (buffer) ZhuMeng();
pc4 = new (buffer + sizeof(ZhuMeng)) ZhuMeng();
使用定位new運算符創建對象時,是使用一個新的對象來覆蓋用於第一個對象的內存單元。因此,如上面兩行代碼所示,必須在原有的地址上有一個偏移量,才能保證兩個部分不重疊。如果類動態地爲成員分配內存,則會出現錯誤。
另外一點是使用delete,如果是用定位new運算符創建的對象,則在刪除的時候,如果使用了delete pc3,則刪掉的是buffer,因爲new/delete系統知道已經分配的是512個字節的內存,而對定位new運算符的操作則是一無所知。由於pc3和buffer指向的內容是一樣的,因此使用pc3刪掉的也是buffer。這樣的話,pc3和pc4的析構函數也不會執行,需要顯式地調用析構函數。
使用delete [] buffer的時候是將整個buffer刪除(對應的new [])而不會爲每一pc調用析構函數(pc3和pc4的析構函數都不會被調用)。
pc4->~ZhuMeng();//釋放部分的內存
pc3->~ZhuMeng();
delete [] buffer;//將整個buffer都給釋放掉
7.類的成員初始化列表
- 成員初始化列表是放在構造函數定義的名稱後面,用冒號指出
ZhuMeng::ZhuMeng(int qs):shi(qs)
{
//......
}
成員初始化列表就是上面代碼片中的
shi(qs)
,將qs賦給shi。打個比方,如果shi是一個const int型的常量,則只能給它初始化而不能給他賦值,如果將
shi = qs
寫在構造函數中,則會報錯,因爲這是賦值,不是初始化,把這個類成員初始化列表寫在構造函數後邊,則會在執行到構造函數之前,也就是創建對象的時候進行初始化,而不是對其進行賦值,這樣就會有很大的好處。只有構造函數可以使用這種初始化列表的方法,並且對於const類的成員,必須使用這種語法,否則,則必須要在類內進行初始化。另外,被聲明爲引用的類成員也必須要使用這種語法。因爲引用與const類似,都是隻能在聲明的時候進行初始化。
在類內進行初始化與使用初始化列表進行初始化是等價的,也就是下面這兩種描述是相同的。
class ZhuMeng{
const int quan = 20;
}
ZhuMeng::ZhuMeng():quan(20)
{
//...
}