類和對象
1. 什麼是面向對象?
面向對象程式設計
概念:(Object Oriented Programming,縮寫:OOP)是一種程式設計範型,同時也是一種程式開發的方法。
對象指的是類的執行個體,将對象作為程式的基本單元,将程式和資料封裝其中,以提高軟體的重用性、靈活性和擴充性。
通俗的了解就是 , 把一種事物起一個名字(類名) , 然後把它有的各種屬性(成員變量)寫出來 ,
再把它能幹的事情(成員函數)寫出來 , 通過類名定義一個它的對象 , 然後這個對象就有這些屬性 , 就可以幹這些事情了 , 通過不同對象的組合 , 進而解決我們的問題
類 (class / struct)
{
成員函數
成員變量
} ;
2. 類的大小?為什麼要記憶體對齊?記憶體對齊的計算?空類的計算
C語言中計算結構體的大小需要記憶體對齊 , 類也一樣
對齊規則為 :
- 第一個成員在結構體變量偏移量為0的位址處
- 其他成員變量要對齊到對齊數的整數倍的位址處
對齊數 = min(編譯器預設的一個對齊數 , 該成員的大小)
VS中預設的值為 8
gcc中的預設值為 4
- 結構體總大小為最大對齊數(每個成員變量除了第一個成員都有一個對齊數)的整數倍
- 如果嵌套了結構體,嵌套的結構體對齊到自己的最大對齊數的整數倍處,結構體的總大小就是所有最大對齊數(含嵌套結構體的對齊數)的整數倍
例如 :
// gcc 對齊數 預設是 4
class A
{
public:
char ch ; // 1 位元組
double d ; // 8 位元組 對齊數是 4
}; // 1000 1111 1111 總大小為 12
class B
{
char ch1 ; // 1 位元組
A a ; // 12 位元組 最大對齊數是 4
char ch2 ; // 1 位元組
}; // 1000 1111 1111 1111 1000 總大小是 20
class C
{
void print()
{
_a = ;
cout << _a << endl;
}
private:
int _a;
};
class D
{
};
void Test04()
{
cout << sizeof(A) << endl; // 12
cout << sizeof(B) << endl; // 20
cout << sizeof(C) << endl; // 4
cout << sizeof(D) << endl; // 1
}
那麼 , 為什麼要存在記憶體對齊呢 ? 這樣不是浪費了很多空間嗎 ?
有兩個原因
-
平台原因(移植原因):
不是所有的硬體平台都能通路任意位址上的任意資料的;
某些硬體平台隻能在某些位址處取某些特定類型的資料,否則抛出硬體異常。
-
性能原因:
資料結構(尤其是棧)應該盡可能地在自然邊界上對齊。
原因在于,為了通路未對齊的記憶體,處理器需要作兩次記憶體通路;
而對齊的記憶體通路僅需要一次通路。
我們以為記憶體是這樣的
其實它是這樣的
CPU 把記憶體當成一塊一塊的 , 塊的大小可以是2,4,8,16位元組大小,是以CPU在讀取記憶體時是一塊一塊進行讀取的。
塊大小稱為
memory access granularity(粒度)
翻譯為 “記憶體讀取粒度”
例如 :
現在要讀取一個
int
類型的變量 , 4 位元組大小 , 如果這個變量存在 0 開始處 , 那麼一次讀 4 個 , 隻用一次就可以讀完 , 同樣如果它在 4 開始 , 也是一次讀完
但是 , 如果沒有記憶體對齊 , 它在 1 開始處 , CPU 隻能先讀取 0 ~ 3 , 再讀取 4 ~ 7 , 然後把 0 和 5 ~ 7 删除 , 得到 1 ~ 4 , 這樣才能讀出這個變量
顯然這樣效率就要差很多 , 是以記憶體對齊可以提高 CPU 讀取記憶體的速度 , 提高效率
3. 類的4個預設成員函數的詳細使用及細節
一個類 ,有 6 個預設的成員函數
- 構造函數
- 拷貝構造函數
- 析構函數
- 指派操作符的重載
- 取位址操作符的重載
修飾的取位址操作符的重載
const
其中最重要的是前 4 個
1. 構造函數
因為成員變量是私有的 , 無法在類外直接通路 , 是以需要一個預設的成員函數來對其進行初始化 , 并且這個預設的成員函數需要在對象被定義的時候自動執行一次 , 這個函數就叫做
構造函數 , 它有一些特點
- 沒有傳回值
- 函數名和類名相同
- 對象執行個體化時, 系統自動調用對應的構造函數
- 可以在類外定義 , 也可以在類中定義
- 如果類中沒有寫構造函數 , 編譯器會生成一個預設的構造函數 , 隻要我們定義了一個構造函數 , 系統就不會再生成預設構造函數
- 無參的構造函數和全預設的構造函數都認為是預設構造函數 , 并且預設構造函數隻能有一個
- 構造函數可以重載
無參的構造函數 和 有參的構造函數
class Date
{
public :
// 1.無參構造函數
Date ()
{
cout << "Date()" << endl;
}
// 2.帶參構造函數
Date (int year, int month , int day )
{
cout << "Date(...)" << endl;
_year = year ;
_month = month ;
_day = day ;
}
private :
int _year ;
int _month ;
int _day ;
};
void TestDate1 ()
{
Date d1 ; // 調用無參構造函數
Date d2 (, , ); // 調用帶參的構造函數
// Date d3 (); // 這種寫法是錯誤的 , 這裡沒有調用 d3 的構造函數定義出 d3
}
帶預設參數的構造函數
class Date
{
public :
// 3.全預設參數的構造函數
/*Date (int year = , int month = , int day = )
{
cout << "Date(.. .. ..)" << endl;
_year = year ;
_month = month ;
_day = day ;
}*/
// 4.半預設參數的構造函數(不常用)
Date (int year, int month = )
{
cout << "Date( .. )" << endl;
_year = year ;
_month = month ;
_day = ;
}
private :
int _year ;
int _month ;
int _day ;
};
void Test()
{
Date d1() ; // 調用半預設構造函數
//Date d2 (2015, 2); // 調用半預設構造函數
}
注意 : 1. 預設參數隻能從右往左定義 2. 如果構造函數的定義和聲明分離 , 既可以在聲明中給預設參數 , 也可以在定義中給
2. 拷貝構造函數
建立對象時用同類的另一個對象來初始化 , 這是調用的構造函數稱為拷貝構造函數 , 拷貝構造函數其實就是構造函數的重載 , 有如下特點 :
- 必須使用引用傳參 , 如果用傳值可能引發無窮遞歸
因為傳值會發生形參到實參的拷貝 , 這個時候又會調用拷貝構造函數 , 調用拷貝構造函數又要發生形參到實參的拷貝 , 又要調拷貝構造函數 ..… 于是就會無窮遞歸
事實上 , 除了傳引用不是傳值外 , 其他傳參方式都是傳值的 , 指針也是 , 隻不過指針傳遞的是對象的位址的值 , 是以拷貝構造隻能傳引用 !
實際中 , 寫成傳值的方式也是編譯不過的
- 如果沒有定義拷貝構造函數 , 系統會自動生成預設的拷貝構造函數 , 它會依次拷貝類的成員進行初始化
class Date
{
public :
Date()
{}
// 拷貝構造函數
Date (const Date &d)
{
_year = d ._year;
_month = d ._month;
_day = d ._day;
}
private :
int _year ;
int _month ;
int _day ;
};
void TestDate1 ()
{
Date d1 ;
// 下面兩種用法都是調用拷貝構造函數,是等價的。
Date d2 (d1); // 調用拷貝構造函數
Date d3 = d1; // 調用拷貝構造函數
}
3. 析構函數
在一個對象的生命周期結束時 , 系統會自動調用一個成員函數來做一些清理工作 , 這個成員函數叫 析構函數 , 有如下特點 :
- 寫法 :
例如~ 類名( )
~Date()
- 沒有參數 , 沒有傳回值
- 在對象的聲明周期結束時自動被系統調用
- 如果自己沒有定義析構函數 , 系統會生成預設的析構函數
- 一個類中 , 析構函數隻能有一個
- 析構函數體内并不是删除這個對象 , 而是做一些清理工作
#include <malloc.h>
class Array
{
public :
Array (int size)
{
cout << "申請空間" << endl;
_ptr = (int *)malloc( size * sizeof (int) );
}
// 這裡的析構函數需要完成的清理工作就是釋放空間
~ Array ()
{
cout << "釋放空間" << endl;
if (_ptr )
{
free(_ptr );
_ptr = ;
}
}
private :
int *_ptr ;
};
void Test05()
{
Array arr();
}
4. 指派操作符的重載
為了增強程式的可讀性 , C++ 支援運算符的重載
運算符重載以後不能改變運算符的優先級 , 結合性 , 操作數
用法 :
傳回類型 operator 運算符 ()
例如 :
void operator+ ()
有 5 個運算符不能被重載 :
: 條件運算符
? :
: 作用域限定符
::
: 成員通路運算符
.
: 成員指針通路運算符
.*
: 長度運算符
sizeof
class Date
{
public :
Date()
{}
// 拷貝構造函數
Date (const Date &d)
: _year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "拷貝構造" << endl;
}
// 指派操作符的重載
// 1.為什麼 operator= 指派函數需要一個 Date& 的傳回值
// 答 : 這樣可以不用調用拷貝構造函數 , 提高效率
// 并且, 預設生成的拷貝構造函數是淺拷貝, 使用傳值傳回的話,
// 如果類中有申請釋放空間之類的操作, 就會出現問題,
// 比如一塊空間被釋放了兩次
Date& operator= (const Date &d)
{
cout << "指派操作符的重載" << endl;
// 2.這裡的if條件判斷是在檢查什麼?
// 答 : 防止自己給自己指派
if (this != &d)
{
this->_year = d. _year;
this->_month = d. _month;
this->_day = d. _day;
}
return *this ;
}
private:
int _year ;
int _month ;
int _day ;
};
void Test06 ()
{
Date d1 ;
Date d2 = d1; // 調用拷貝構造函數
Date d3 ;
d3 = d1 ; // 調用指派運算符的重載
}
當類的對象需要拷貝時,拷貝構造函數将會被調用。
以下情況都會調用拷貝構造函數 :
- 一個對象以值傳遞的方式傳入函數體
- 一個對象以值傳遞的方式從函數傳回
- 一個對象需要通過另外一個對象進行初始化
在類中沒有定義拷貝構造函數時 , 系統會預設生成拷貝構造函數 , 這個拷貝構造函數完成對象之間的淺拷貝
class TestCls{
public:
int a;
int *p;
public:
TestCls(int _a = ) //無參構造函數
:a(_a)
{
std::cout<<"TestCls()"<<std::endl;
// p = new int;
}
~TestCls() //析構函數
{
// delete p;
std::cout<<"~TestCls()"<<std::endl;
}
void Set_a(int _a)
{
a = _a;
}
void print()
{
cout << a << endl;
cout << &a << endl;
cout << p << endl;
}
private:
// 這樣這個類就不可以被拷貝了
TestCls(const TestCls& ts)
{}
};
void Test07()
{
TestCls tc1();
TestCls tc2 = tc1;
tc1.print();
tc2.print();
tc2.Set_a();
tc1.print();
tc2.print();
}
成員變量的初始化有兩種方式 :
- 構造函數體内進行指派
- 初始化清單
其中 初始化清單 更加高效
用法 :
Date(int t_year, int t_month, int t_day)
:m_year(t_year), m_month(t_month), m_day(t_day)
{}
為什麼初始化清單更加高效 ?
因為即使不用初始化清單 , 這一步也會執行一次 , 是以用了初始化清單就免去了函數體内進行指派的操作 , 進而效率更高
有一些成員變量必須用初始化清單進行初始化
成員變量
const
- 引用類型的成員變量
- 沒有預設構造函數的類成員變量
注意 : 成員變量的初始化是按聲明的順序進行初始化的 , 而非初始化清單的順序
class Time
{
public :
Time (const Time &t)
{
cout << "Time (const Time& t)" << endl;
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
private:
int _hour ;
int _minute ;
int _second ;
};
class Date
{
public :
Date (int year, int month , int day, const Time &t)
:_testConst(100), _testReference(_year), _t(t)
{
cout << "Date ()" << endl;
_year = year ;
_month = month ;
_day = day ;
_t = t ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
const int _testConst; // 1.測試 const 成員變量的初始化
int &_testReference ; // 2.測試引用成員變量的初始化
Time _t ; // 3.測試無預設構造函數的成員變量的初始化
};
void Test08()
{
Time tm;
Date dt(, , , tm);
}