【黑马程序员 C++教程从0到1入门编程】【笔记4】C++核心编程(类和对象——封装、权限、对象的初始化和清理、构造函数、析构函数、深拷贝、浅拷贝、初始化列表、友元friend、运算符重载)
黑馬程序員C++教程
文章目錄
- 4 類和對象(類屬性【成員屬性】,類函數【成員函數】)
- 4.1 封裝
- 4.1.1 封裝的意義(三種權限:public公共、protected保護、private私有)(將屬性和行為作為一個整體,表現生活中的事物;將屬性和行為加以權限控制)(`class 類名{ 訪問權限: 屬性 / 行為 };`)
- 示例1:設計一個圓類,求圓的周長
- 示例2:設計一個學生類,屬性有姓名和學號,可以給姓名和學號賦值,可以顯示學生的姓名和學號
- 三種權限示例(雖然類外不可訪問類內的保護權限和私有權限元素,但是可以通過類內提供的公共方法進行設置【初始化】)(私有權限不能在子類中訪問,保護權限可以在子類中訪問)
- 4.1.2 struct和class區別(struct 默認權限為公共,class 默認權限為私有【class不指定訪問權限時】)
- 4.1.3 成員屬性設置為私有(成員私有屬性只有它自己的方法能修改和讀取)(然后在類內用方法實現對屬性的——讀、寫等功能)
- 4.2 對象的初始化和清理
- 4.2.1 構造函數(constructor `類名(){}`)和析構函數(destructor `~類名(){}`)(寫在類里面)(如果對象不是用new方法創建的,則函數塊結束后系統會自動調用(自己寫的或系統生成的)析構函數釋放內存;如果用new方法創建,則調用delete方法時會調用析構函數)(如果不寫析構函數,貌似也沒事,系統會自動生成構造函數和析構函數。。。這塊內存也不會被一直霸占著。。而且如果不寫析構函數,delete方法也能釋放用new方法創建的對象。。。總而言之,就是如果你不寫,系統會自動幫你寫)
- 4.2.2 構造函數的分類及調用(有參構造和無參構造)(普通構造和拷貝構造)(三種調用方式:括號法、顯示法、隱式轉換法【不知道多參的情況怎么隱式轉換??】)
- 4.2.3 拷貝構造函數調用時機(使用一個已經創建完畢的對象來初始化一個新對象、值傳遞的方式給函數參數傳值、以值方式返回局部對象)
- 4.2.4 構造函數調用規則(如果用戶定義有參構造函數,c++不再提供默認無參構造,但是會提供默認拷貝構造;如果用戶定義拷貝構造函數,c++不會再提供其他構造函數)
- 4.2.5 深拷貝與淺拷貝(淺拷貝:簡單的賦值拷貝操作、深拷貝:在堆區重新申請空間,進行拷貝操作)(delete()只能刪除在堆區開辟空間的指針)
- 4.2.6 初始化列表(寫在構造函數的參數和大括號之間,用冒號:開始,格式為類型(初始化參數),不同項之間用逗號,隔開)(`構造函數():屬性1(值1),屬性2(值2)... {}`)
- 4.2.7 類對象作為類成員(初始化列表可以告訴編譯器調用哪一個構造函數)
- 4.2.8 靜態成員 static(包括靜態變量和靜態函數)(類內聲明,類外初始化)(靜態成員變量:所有對象共享同一份數據,在編譯階段分配內存,類內聲明,類外初始化)( 靜態成員函數:所有對象共享同一個函數,靜態成員函數只能訪問靜態成員變量)
- 靜態成員變量能在類內初始化嗎?(靜態成員函數可以,變量不行)
- 4.3 C++對象模型和this指針
- 4.3.1 成員變量和成員函數分開存儲(只有非靜態成員變量占對象空間【成員函數,靜態變量和靜態函數都不占對象空間】)
- 4.3.2 this指針概念(this確實是個指針!)
- 4.3.3 空指針訪問成員函數(照理來說也是,類的成員函數跟它的實例對象沒關系)
- 4.3.4 const修飾成員函數(常函數:在參數括號后和大括號之間加const)(什么意思,我越看越懵逼了!)(別蒙蔽,仔細看!)(常函數內不可以修改成員屬性;成員屬性聲明時加關鍵字mutable后,在常函數中依然可以修改)(常對象:聲明對象前加const稱該對象為常對象;常對象只能調用常函數,只能修改帶mutable關鍵字的成員屬性)
- 4.4 友元 friend(讓一個函數或者類 訪問另一個類中私有成員)
- 4.4.1 全局函數做友元
- 4.4.2 類做友元(就是不知道干啥要這樣設計,繞來繞去的,有個啥意思?)(我又看了一遍還是懵逼,需要看視頻?)
- 4.4.3 成員函數做友元
- 4.5 運算符重載(對已有的運算符重新進行定義,賦予其另一種功能,以適應不同的數據類型)
- 可重載運算符和不可重載運算符
- 4.5.1 加號運算符重載(operator+)
- 4.5.2 左移運算符重載(輸出自定義數據類型)(ostream& operator<<)
- 下面未檢測-------------------------------上面需再次看視頻,因為確實很多知識文檔里沒講到而視頻里講到了
- 4.5.3 遞增運算符重載
- 4.5.4 賦值運算符重載(對屬性值進行拷貝)(這個用得多不多啊,搞得語法有點亂不是么?)
- 4.5.5 關系運算符重載(靈活是靈活,但是用普通函數也可以實現呀!)
- 4.5.6 函數調用運算符重載(仿函數)(如果要理解還得看視頻,沒看,先過了。。。)
4 類和對象(類屬性【成員屬性】,類函數【成員函數】)
C++面向對象的三大特性為:封裝、繼承、多態
C++認為萬事萬物都皆為對象,對象上有其屬性和行為
例如:
? 人可以作為對象,屬性有姓名、年齡、身高、體重…,行為有走、跑、跳、吃飯、唱歌…
? 車也可以作為對象,屬性有輪胎、方向盤、車燈…,行為有載人、放音樂、放空調…
? 具有相同性質的對象,我們可以抽象稱為類,人屬于人類,車屬于車類
4.1 封裝
4.1.1 封裝的意義(三種權限:public公共、protected保護、private私有)(將屬性和行為作為一個整體,表現生活中的事物;將屬性和行為加以權限控制)(class 類名{ 訪問權限: 屬性 / 行為 };)
封裝是C++面向對象三大特性之一
封裝的意義:
- 將屬性和行為作為一個整體,表現生活中的事物
- 將屬性和行為加以權限控制
封裝意義一:
? 在設計類的時候,屬性和行為寫在一起,表現事物
語法: class 類名{ 訪問權限: 屬性 / 行為 };
(如果權限相同,屬性和行為能寫在一起,也能分開寫)
示例1:設計一個圓類,求圓的周長
示例代碼:
#include<iostream> #include<string> using namespace std;//圓周率 const double PI = 3.14;//1、封裝的意義 //將屬性和行為作為一個整體,用來表現生活中的事物//封裝一個圓類,求圓的周長 //class代表設計一個類,后面跟著的是類名 class Circle { public: //訪問權限 公共的權限//屬性int m_r;//半徑//行為//獲取到圓的周長double calculateZC(){//2 * pi * r//獲取圓的周長return 2 * PI * m_r;} };int main() {//通過圓類,創建圓的對象// c1就是一個具體的圓Circle c1;c1.m_r = 10; //給圓對象的半徑 進行賦值操作//2 * pi * 10 = = 62.8cout << "圓的周長為: " << c1.calculateZC() << endl; //62.8system("pause");return 0; }示例2:設計一個學生類,屬性有姓名和學號,可以給姓名和學號賦值,可以顯示學生的姓名和學號
示例2代碼:
#include<iostream> #include<string> using namespace std;//學生類 class Student { public:void setName(string name) {m_name = name;}void setID(int id) {m_id = id;}void showStudent() {cout << "name:" << m_name << " ID:" << m_id << endl;}public:string m_name;int m_id; };int main() {Student stu;stu.setName("德瑪西亞");stu.setID(250);stu.showStudent(); //name:德瑪西亞 ID:250system("pause");return 0; } 習慣上寫兩個public,一個寫屬性,一個寫方法封裝意義二:
類在設計時,可以把屬性和行為放在不同的權限下,加以控制
訪問權限有三種:
三種權限示例(雖然類外不可訪問類內的保護權限和私有權限元素,但是可以通過類內提供的公共方法進行設置【初始化】)(私有權限不能在子類中訪問,保護權限可以在子類中訪問)
#include<iostream> #include<string> using namespace std;//三種權限 //公共權限 public 類內可以訪問 類外可以訪問 //保護權限 protected 類內可以訪問 類外不可以訪問 //私有權限 private 類內可以訪問 類外不可以訪問class Person {//姓名 公共權限 public:string m_Name;//汽車 保護權限 protected:string m_Car;//銀行卡密碼 私有權限 private:int m_Password;//相當于初始化 public:void func(){m_Name = "張三";m_Car = "拖拉機";m_Password = 123456;} };int main() {Person p;p.m_Name = "李四";//p.m_Car = "奔馳"; //保護權限類外訪問不到//p.m_Password = 123; //私有權限類外訪問不到system("pause");return 0; }4.1.2 struct和class區別(struct 默認權限為公共,class 默認權限為私有【class不指定訪問權限時】)
在C++中 struct和class唯一的區別就在于 默認的訪問權限不同
區別:
- struct 默認權限為公共
- class 默認權限為私有
4.1.3 成員屬性設置為私有(成員私有屬性只有它自己的方法能修改和讀取)(然后在類內用方法實現對屬性的——讀、寫等功能)
**優點1:**將所有成員屬性設置為私有,可以自己控制讀寫權限
**優點2:**對于寫權限,我們可以檢測數據的有效性(通過類內方法實現)
示例:
#include<iostream> #include<string> using namespace std;class Person { public://姓名設置可讀可寫void setName(string name) {m_Name = name;}string getName(){return m_Name;}//設置年齡void setAge(int age) {if (age < 0 || age > 150) {cout << "你個老妖精!" << endl;return;}m_Age = age;}//獲取年齡 int getAge() {return m_Age;}//情人設置為只寫void setLover(string lover) {m_Lover = lover;}private:string m_Name; //可讀可寫 姓名int m_Age; //可讀可寫 年齡string m_Lover; //只寫 情人 };int main() {Person p;//姓名設置p.setName("張三");cout << "姓名: " << p.getName() << endl;//年齡設置p.setAge(50);cout << "年齡: " << p.getAge() << endl;//情人設置p.setLover("蒼井");//cout << "情人: " << p.m_Lover << endl; //只寫屬性,不可以讀取system("pause");return 0; }4.2 對象的初始化和清理
- 生活中我們買的電子產品都基本會有出廠設置,在某一天我們不用時候也會刪除一些自己信息數據保證安全
- C++中的面向對象來源于生活,每個對象也都會有初始設置以及 對象銷毀前的清理數據的設置。
4.2.1 構造函數(constructor 類名(){})和析構函數(destructor ~類名(){})(寫在類里面)(如果對象不是用new方法創建的,則函數塊結束后系統會自動調用(自己寫的或系統生成的)析構函數釋放內存;如果用new方法創建,則調用delete方法時會調用析構函數)(如果不寫析構函數,貌似也沒事,系統會自動生成構造函數和析構函數。。。這塊內存也不會被一直霸占著。。而且如果不寫析構函數,delete方法也能釋放用new方法創建的對象。。??偠灾?#xff0c;就是如果你不寫,系統會自動幫你寫)
(疑問:什么情況下才寫析構函數?)
析構函數(destructor)
與構造函數相反,當對象結束其生命周期,如對象所在的函數已調用完畢時,系統自動執行析構函數。析構函數往往用來做“清理善后”
的工作(例如在建立對象時用new開辟了一片內存空間,delete會自動調用析構函數后釋放內存)。
對象的初始化和清理也是兩個非常重要的安全問題
? 一個對象或者變量沒有初始狀態,對其使用后果是未知的
? 同樣的使用完一個對象或變量,沒有及時清理,也會造成一定的安全問題
c++利用了構造函數和析構函數解決上述問題,這兩個函數將會被編譯器自動調用,完成對象初始化和清理工作。
對象的初始化和清理工作是編譯器強制要我們做的事情,因此如果我們不提供構造和析構,編譯器會提供
編譯器提供的構造函數和析構函數是空實現(?)。
- 構造函數:主要作用在于創建對象時為對象的成員屬性賦值,構造函數由編譯器自動調用,無須手動調用。
- 析構函數:主要作用在于對象銷毀前系統自動調用,執行一些清理工作(清理啥?。。。以后你會知道的)。
構造函數語法:類名(){}
析構函數語法: ~類名(){}
運行結果:
Person的構造函數調用 Person的析構函數調用4.2.2 構造函數的分類及調用(有參構造和無參構造)(普通構造和拷貝構造)(三種調用方式:括號法、顯示法、隱式轉換法【不知道多參的情況怎么隱式轉換??】)
兩種分類方式:
? 按參數分為: 有參構造和無參構造
? 按類型分為: 普通構造和拷貝構造
三種調用方式:
? 括號法
? 顯示法
? 隱式轉換法
示例:
#include<iostream> #include<string> using namespace std;//1、構造函數分類 // 按照參數分類分為 有參和無參構造 無參又稱為默認構造函數 // 按照類型分類分為 普通構造和拷貝構造class Person { public://無參(默認)構造函數Person() {cout << "無參構造函數!" << endl;}//有參構造函數1Person(int a) {age = a;cout << "有參構造函數1!" << endl;}//有參構造函數2Person(int a, int b) {age = a + b;cout << "有參構造函數2!" << endl;}//拷貝構造函數(不加const還不行!)//(當然你也可以通過傳入Person*的方式,這里只是傳入引用防止對被引用Person對象更改)Person(const Person& p) {age = p.age;cout << "拷貝構造函數!" << endl;}//析構函數~Person() {cout << this << endl;cout << "析構函數!" << endl;} public:int age; };//2、構造函數的調用 //調用無參構造函數 void test01() {Person p; //調用無參構造函數 }//調用有參的構造函數 void test02() {//2.1 括號法,常用Person p1(10);Person p1_(10, 20);//注意1:調用無參構造函數不能加括號,如果加了編譯器認為這是一個函數聲明//Person p2();//2.2 顯式法Person p2 = Person(10);Person p3 = Person(p2);//Person(10)單獨寫就是匿名對象 當前行結束之后,馬上析構//2.3 隱式轉換法Person p4 = 10; // Person p4 = Person(10); //不能用這種方法進行多參數隱式轉換啊?!//Person p4_ = (10, 20);Person p5 = p4; // Person p5 = Person(p4); //注意2:不能利用 拷貝構造函數 初始化匿名對象 編譯器認為是對象聲明//Person p5(p4); }//函數塊結束時會調用析構函數,析構順序是創建順序的逆序int main() {//test01();test02();system("pause");return 0; }調試發現,每個類對象銷毀時都會調用一次析構函數
運行結果:
有參構造函數1! 有參構造函數2! 有參構造函數1! 拷貝構造函數! 有參構造函數1! 拷貝構造函數! 0099F9AC 析構函數! 0099F9B8 析構函數! 0099F9C4 析構函數! 0099F9D0 析構函數! 0099F9DC 析構函數! 0099F9E8 析構函數! 請按任意鍵繼續. . .4.2.3 拷貝構造函數調用時機(使用一個已經創建完畢的對象來初始化一個新對象、值傳遞的方式給函數參數傳值、以值方式返回局部對象)
C++中拷貝構造函數調用時機通常有三種情況
- 使用一個已經創建完畢的對象來初始化一個新對象
- 值傳遞的方式給函數參數傳值
- 以值方式返回局部對象
示例:
#include<iostream> #include<string> using namespace std;class Person { public:Person() {cout << "無參構造函數!" << endl;mAge = 0;}Person(int age) {cout << "有參構造函數!" << endl;mAge = age;}Person(const Person& p) {cout << "拷貝構造函數!" << endl;mAge = p.mAge;}//析構函數在釋放內存之前調用~Person() {cout << "析構函數!" << endl;} public:int mAge; };//1. 使用一個已經創建完畢的對象來初始化一個新對象 void test01() {Person man(100); //p對象已經創建完畢Person newman(man); //調用拷貝構造函數Person newman2 = man; //拷貝構造Person newman3;newman3 = man; //不是調用拷貝構造函數,賦值操作(Ar:一模一樣復制過去) }//2. 值傳遞的方式給函數參數傳值 //相當于Person p1 = p;(Ar:實參復制到形參??) void doWork(Person p1) {} void test02() {Person p; //無參構造函數doWork(p); }//3. 以值方式返回局部對象 Person doWork2() {Person p1;cout << (int*)&p1 << endl;//cout << &p1 << endl; //跟上面一樣的return p1; }void test03() {Person p = doWork2();cout << (int*)&p << endl;//cout << &p << endl; //跟上面一樣的 }int main() {test01();test02();test03();system("pause");return 0; }運行結果:
有參構造函數! 拷貝構造函數! 拷貝構造函數! 無參構造函數! 析構函數! 析構函數! 析構函數! 析構函數! 無參構造函數! 拷貝構造函數! 析構函數! 析構函數! 無參構造函數! 00AFF6FC 拷貝構造函數! 析構函數! 00AFF7F4 析構函數!4.2.4 構造函數調用規則(如果用戶定義有參構造函數,c++不再提供默認無參構造,但是會提供默認拷貝構造;如果用戶定義拷貝構造函數,c++不會再提供其他構造函數)
默認情況下,c++編譯器至少給一個類添加3個函數
1.默認構造函數(無參,函數體為空)
2.默認析構函數(無參,函數體為空)
3.默認拷貝構造函數,對屬性進行值拷貝
構造函數調用規則如下:
-
如果用戶定義有參構造函數,c++不再提供默認無參構造,但是會提供默認拷貝構造
-
如果用戶定義拷貝構造函數,c++不會再提供其他構造函數
示例:
#include<iostream> #include<string> using namespace std;class Person { public://無參(默認)構造函數Person() {cout << "無參構造函數!" << endl;}//有參構造函數Person(int a) {age = a;cout << "有參構造函數!" << endl;}//拷貝構造函數Person(const Person& p) {age = p.age;cout << "拷貝構造函數!" << endl;}//析構函數~Person() {cout << "析構函數!" << endl;} public:int age; };void test01() {Person p1(18);//如果不寫拷貝構造,編譯器會自動添加拷貝構造,并且做淺拷貝操作(Ar:我調試也沒發現是淺拷貝呀??【引用】。。。是淺拷貝)Person p2(p1);//p1.age = 19;//p2.age = 21;cout << "p2的年齡為: " << p2.age << endl; //18 }void test02() {//如果用戶提供有參構造,編譯器不會提供默認構造,會提供拷貝構造Person p1; //此時如果用戶自己沒有提供默認構造,會出錯(Ar:VS下編譯都編譯不過)Person p2(10); //用戶提供的有參Person p3(p2); //此時如果用戶沒有提供拷貝構造,編譯器會提供(Ar:編譯器提供的貌似是全部復制)//如果用戶提供拷貝構造,編譯器不會提供其他構造函數Person p4; //此時如果用戶自己沒有提供默認構造,會出錯Person p5(10); //此時如果用戶自己沒有提供有參,會出錯Person p6(p5); //用戶自己提供拷貝構造 }int main() {test01();test02();system("pause");return 0; }運行結果:
有參構造函數! 拷貝構造函數! p2的年齡為: 18 析構函數! 析構函數! 無參構造函數! 有參構造函數! 拷貝構造函數! 無參構造函數! 有參構造函數! 拷貝構造函數! 析構函數! 析構函數! 析構函數! 析構函數! 析構函數! 析構函數!4.2.5 深拷貝與淺拷貝(淺拷貝:簡單的賦值拷貝操作、深拷貝:在堆區重新申請空間,進行拷貝操作)(delete()只能刪除在堆區開辟空間的指針)
深淺拷貝是面試經典問題,也是常見的一個坑
淺拷貝:簡單的賦值拷貝操作
深拷貝:在堆區重新申請空間,進行拷貝操作
示例:
#include<iostream> #include<string> using namespace std;class Person { public://無參(默認)構造函數Person() {cout << "無參構造函數!" << endl;}//有參構造函數Person(int age, int height) {cout << "有參構造函數!" << endl;m_age = age;m_height = new int(height);}//拷貝構造函數 Person(const Person& p) {cout << "拷貝構造函數!" << endl;//如果不利用深拷貝在堆區創建新內存,會導致淺拷貝帶來的重復釋放堆區問題//(Ar:就是說兩個不同Person對象的m_height指針指向的是同一塊內存?)m_age = p.m_age;m_height = new int(*p.m_height);//m_height = p.m_height; //(這是錯的!不該這么做!)}//析構函數~Person() {cout << "析構函數!" << endl;if (m_height != NULL){delete m_height;}} public:int m_age;int* m_height; };void test01() {Person p1(18, 180);Person p2(p1);cout << "p1的年齡: " << p1.m_age << " 身高: " << *p1.m_height << endl;cout << "p2的年齡: " << p2.m_age << " 身高: " << *p2.m_height << endl; }int main() {test01();system("pause");return 0; }運行結果:
有參構造函數! 拷貝構造函數! p1的年齡: 18 身高: 180 p2的年齡: 18 身高: 180 析構函數! 析構函數!4.2.6 初始化列表(寫在構造函數的參數和大括號之間,用冒號:開始,格式為類型(初始化參數),不同項之間用逗號,隔開)(構造函數():屬性1(值1),屬性2(值2)... {})
作用:
C++提供了初始化列表語法,用來初始化屬性
語法:構造函數():屬性1(值1),屬性2(值2)... {}
示例:
#include<iostream> #include<string> using namespace std;class Person { public:傳統方式初始化//Person(int a, int b, int c) {// m_A = a;// m_B = b;// m_C = c;//}//初始化列表方式初始化Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}void PrintPerson() {cout << "mA:" << m_A << endl;cout << "mB:" << m_B << endl;cout << "mC:" << m_C << endl;} private:int m_A;int m_B;int m_C; };int main() {Person p(1, 2, 3);p.PrintPerson();system("pause");return 0; } mA:1 mB:2 mC:34.2.7 類對象作為類成員(初始化列表可以告訴編譯器調用哪一個構造函數)
C++類中的成員可以是另一個類的對象,我們稱該成員為 對象成員
例如:
class A {} class B {A a; //B類的成員,A類的對象 }B類中有對象A作為成員,A為對象成員
那么當創建B對象時,A與B的構造和析構的順序是誰先誰后?
構造的順序是 :先調用對象成員的構造,再調用本類構造
析構順序是:先析構本類構造,再析構對象成員構造
示例:
#include<iostream> #include<string> using namespace std;class Phone { public:Phone(string name){m_PhoneName = name;cout << "Phone構造" << endl;}~Phone(){cout << "Phone析構" << endl;}string m_PhoneName;};class Person { public://初始化列表可以告訴編譯器調用哪一個構造函數【m_Phone(pName)】Person(string name, string pName) :m_Name(name), m_Phone(pName){cout << "Person構造" << endl;}~Person(){cout << "Person析構" << endl;}void playGame(){cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手機! " << endl;}string m_Name;Phone m_Phone;};void test01() {//當類中成員是其他類對象時,我們稱該成員為 對象成員//構造的順序是 :先調用對象成員的構造,再調用本類構造//析構順序與構造相反Person p("張三", "蘋果X");p.playGame();}int main() {test01();system("pause");return 0; }運行結果:
Phone構造 Person構造 張三 使用蘋果X 牌手機! Person析構 Phone析構4.2.8 靜態成員 static(包括靜態變量和靜態函數)(類內聲明,類外初始化)(靜態成員變量:所有對象共享同一份數據,在編譯階段分配內存,類內聲明,類外初始化)( 靜態成員函數:所有對象共享同一個函數,靜態成員函數只能訪問靜態成員變量)
靜態成員就是在成員變量和成員函數前加上關鍵字static,稱為靜態成員
靜態成員分為:
- 靜態成員變量
- 所有對象共享同一份數據
- 在編譯階段分配內存
- 類內聲明,類外初始化
- 靜態成員函數
- 所有對象共享同一個函數
- 靜態成員函數只能訪問靜態成員變量
示例1:靜態成員變量
#include<iostream> #include<string> using namespace std;class Person {public:static int m_A; //靜態成員變量//靜態成員變量特點://1 在編譯階段分配內存//2 類內聲明,類外初始化//3 所有對象共享同一份數據//我就想試試如果m_B不初始化,用類內函數調用會怎么樣(結果當然是編譯不過啦by Ar)//void PPP() {// printf("%d\n", m_B);//}private:static int m_B; //靜態成員變量也是有訪問權限的 };//Ar 竟然不加類型修飾符還不行(去掉int報錯),它難道不知道m_A是int嗎?? int Person::m_A = 10; //類內聲明,類外初始化(必須初始化,否則有引用時編譯不過)(Arnold) //int Person::m_B = 10;void test01() {//靜態成員變量兩種訪問方式//1、通過對象Person p1;//p1.PPP();p1.m_A = 100;cout << "p1.m_A = " << p1.m_A << endl;Person p2;p2.m_A = 200;cout << "p1.m_A = " << p1.m_A << endl; //共享同一份數據cout << "p2.m_A = " << p2.m_A << endl;//2、通過類名cout << "m_A = " << Person::m_A << endl;//cout << "m_B = " << Person::m_B << endl; //私有權限訪問不到 }int main() {test01();system("pause");return 0; }運行結果:
p1.m_A = 100 p1.m_A = 200 p2.m_A = 200 m_A = 200靜態成員變量能在類內初始化嗎?(靜態成員函數可以,變量不行)
不能
static成員變量和普通static變量一樣,都在內存分區的全局數據區分配內存,到程序結束后釋放。這就意味著,static 成員變量不隨對象的創建而分配內存,也不隨對象的銷毀而釋放內存。而普通成員變量在對象創建時分配內存,在對象銷毀時釋放內存
參考文章:C++中靜態成員變量的可以在類內初始化嗎?
示例2:靜態成員函數(靜態成員函數只能訪問靜態成員變量)
#include<iostream> #include<string> using namespace std;class Person {public://靜態成員函數特點://1 程序共享一個函數//2 靜態成員函數只能訪問靜態成員變量static void func(){cout << "func調用" << endl;m_A = 100;//m_B = 100; //錯誤,不可以訪問非靜態成員變量}static int m_A; //靜態成員變量int m_B; // private://靜態成員函數也是有訪問權限的static void func2(){cout << "func2調用" << endl;} }; int Person::m_A = 10;void test01() {//靜態成員變量兩種訪問方式//1、通過對象Person p1;p1.func();//2、通過類名Person::func();//Person::func2(); //私有權限訪問不到 }int main() {test01();system("pause");return 0; } func調用 func調用4.3 C++對象模型和this指針
4.3.1 成員變量和成員函數分開存儲(只有非靜態成員變量占對象空間【成員函數,靜態變量和靜態函數都不占對象空間】)
在C++中,類內的成員變量和成員函數分開存儲
只有非靜態成員變量才屬于類的對象上
#include<iostream> #include<string> using namespace std;class Person { public:Person() {mA = 0;}//非靜態成員變量占對象空間int mA;//靜態成員變量不占對象空間static int mB;//函數也不占對象空間,所有函數共享一個函數實例void func() {cout << "mA:" << this->mA << endl;}//靜態成員函數也不占對象空間static void sfunc() {} };int main() {cout << sizeof(Person) << endl; //4system("pause");return 0; }靜態成員函數和普通成員函數的區別
靜態函數不傳遞this指針,不識別對象個體4.3.2 this指針概念(this確實是個指針!)
通過4.3.1我們知道在C++中成員變量和成員函數是分開存儲的
每一個非靜態成員函數只會誕生一份函數實例,也就是說多個同類型的對象會共用一塊代碼
那么問題是:這一塊代碼是如何區分那個對象調用自己的呢?
c++通過提供特殊的對象指針,this指針,解決上述問題。this指針指向被調用的成員函數所屬的對象
this指針是隱含每一個非靜態成員函數內的一種指針
this指針不需要定義,直接使用即可
this指針的用途:
- 當形參和成員變量同名時,可用this指針來區分
- 在類的非靜態成員函數中返回對象本身,可使用return *this
運行結果:
p1.age = 10 0133F730 0133F730 0133F730 p2.age = 404.3.3 空指針訪問成員函數(照理來說也是,類的成員函數跟它的實例對象沒關系)
C++中空指針也是可以調用成員函數的,但是也要注意有沒有用到this指針
如果用到this指針,需要加以判斷保證代碼的健壯性
示例:
#include<iostream> #include<string> using namespace std;//空指針訪問成員函數 class Person { public:void ShowClassName() {cout << "我是Person類!" << endl;}void ShowPerson() {if (this == NULL) {return;}cout << mAge << endl;}public:int mAge; };void test01() {Person* p = NULL;p->ShowClassName(); //空指針,可以調用成員函數p->ShowPerson(); //但是如果成員函數中用到了this指針,就不可以了 }int main() {test01();system("pause");return 0; } 我是Person類!4.3.4 const修飾成員函數(常函數:在參數括號后和大括號之間加const)(什么意思,我越看越懵逼了!)(別蒙蔽,仔細看!)(常函數內不可以修改成員屬性;成員屬性聲明時加關鍵字mutable后,在常函數中依然可以修改)(常對象:聲明對象前加const稱該對象為常對象;常對象只能調用常函數,只能修改帶mutable關鍵字的成員屬性)
常函數:
- 成員函數后加const后我們稱為這個函數為常函數
- 常函數內不可以修改成員屬性
- 成員屬性聲明時加關鍵字mutable后,在常函數中依然可以修改
常對象:
- 聲明對象前加const稱該對象為常對象
- 常對象只能調用常函數
示例:
#include<iostream> #include<string> using namespace std;class Person { public:Person() {m_A = 0;m_B = 0;}//this指針的本質是一個指針常量,指針的指向不可修改//如果想讓指針指向的值也不可以修改,需要聲明常函數void ShowPerson() const {//const Type* const pointer;//this = NULL; //不能修改指針的指向 Person* const this;//this->m_A = 100; //沒加mutable不能修改//const修飾成員函數,表示指針指向的內存空間的數據不能修改,除了mutable修飾的變量this->m_B = 100;}void MyFunc() const {//m_A = 10000; //報錯,無法修改cout << m_A << endl; //但是可以訪問m_B = 11234;}void MyFunc2(){}public:int m_A;mutable int m_B; //可修改 可變的 };//const修飾對象 常對象 void test01() {const Person person; //常量對象 cout << person.m_A << endl;//person.mA = 100; //常對象不能修改成員變量的值,但是可以訪問person.m_B = 100; //但是常對象可以修改mutable修飾成員變量//常對象訪問成員函數person.MyFunc(); //常對象只能調用const的函數//person.MyFunc2(); //報錯:不能訪問(常對象只能調用常函數)//person.m_A = 1111; //報錯:表達式必須是可修改的左值(常對象只能修改帶mutable關鍵字的成員屬性)}int main() {test01();system("pause");return 0; }運行結果:
0 04.4 友元 friend(讓一個函數或者類 訪問另一個類中私有成員)
生活中你的家有客廳(Public),有你的臥室(Private)
客廳所有來的客人都可以進去,但是你的臥室是私有的,也就是說只有你能進去
但是呢,你也可以允許你的好閨蜜好基友進去。
在程序里,有些私有屬性 也想讓類外特殊的一些函數或者類進行訪問,就需要用到友元的技術
友元的目的就是讓一個函數或者類 訪問另一個類中私有成員
友元的關鍵字為 friend
友元的三種實現
- 全局函數做友元
- 類做友元
- 成員函數做友元
4.4.1 全局函數做友元
#include<iostream> #include<string> using namespace std;class Building {//告訴編譯器 goodGay全局函數 是 Building類的好朋友,可以訪問類中的私有內容(相當于函數聲明)friend void goodGay(Building* building);public:Building(){this->m_SittingRoom = "客廳";this->m_BedRoom = "臥室";}public:string m_SittingRoom; //客廳private:string m_BedRoom; //臥室 };void goodGay(Building* building) {cout << "好基友正在訪問: " << building->m_SittingRoom << endl;cout << "好基友正在訪問: " << building->m_BedRoom << endl; }void test01() {Building b;goodGay(&b); }int main() {test01();system("pause");return 0; }運行結果:
好基友正在訪問: 客廳 好基友正在訪問: 臥室4.4.2 類做友元(就是不知道干啥要這樣設計,繞來繞去的,有個啥意思?)(我又看了一遍還是懵逼,需要看視頻?)
C++有的地方為什么要類內定義,類外實現(類內聲明,類外初始化)?
#include<iostream> #include<string> using namespace std;class Building; //前面的調用后面的,要把后面的弄到前面來聲明一下 class goodGay { public:goodGay();void visit();private:Building* building; };class Building {//告訴編譯器 goodGay類是Building類的好朋友,可以訪問到Building類中私有內容friend class goodGay;public:Building();public:string m_SittingRoom; //客廳 private:string m_BedRoom;//臥室 };Building::Building() {this->m_SittingRoom = "客廳";this->m_BedRoom = "臥室"; }goodGay::goodGay() //類內聲明,類外初始化 {building = new Building; }void goodGay::visit() {cout << "好基友正在訪問" << building->m_SittingRoom << endl;cout << "好基友正在訪問" << building->m_BedRoom << endl; }void test01() {goodGay gg;gg.visit();}int main() {test01();system("pause");return 0; } 好基友正在訪問: 客廳 好基友正在訪問: 臥室4.4.3 成員函數做友元
#include<iostream> #include<string> using namespace std;class Building; class goodGay { public:goodGay();void visit(); //只讓visit函數作為Building的好朋友,可以發訪問Building中私有內容void visit2();private:Building* building; };class Building {//告訴編譯器 goodGay類中的visit成員函數 是Building好朋友,可以訪問私有內容friend void goodGay::visit();public:Building();public:string m_SittingRoom; //客廳 private:string m_BedRoom;//臥室 };Building::Building() {this->m_SittingRoom = "客廳";this->m_BedRoom = "臥室"; }goodGay::goodGay() {building = new Building; }void goodGay::visit() {cout << "好基友正在訪問" << building->m_SittingRoom << endl;cout << "好基友正在訪問" << building->m_BedRoom << endl; }void goodGay::visit2() {cout << "好基友正在訪問" << building->m_SittingRoom << endl;//cout << "好基友正在訪問" << building->m_BedRoom << endl; //visit2不是好朋友,不能訪問私有內容(臥室) }void test01() {goodGay gg;gg.visit();gg.visit2(); }int main() {test01();system("pause");return 0; } 好基友正在訪問客廳 好基友正在訪問臥室 好基友正在訪問客廳4.5 運算符重載(對已有的運算符重新進行定義,賦予其另一種功能,以適應不同的數據類型)
可重載運算符和不可重載運算符
運算符重載概念:對已有的運算符重新進行定義,賦予其另一種功能,以適應不同的數據類型
4.5.1 加號運算符重載(operator+)
作用:實現兩個自定義數據類型相加的運算
#include<iostream> #include<string> using namespace std;class Person { public:Person() {};Person(int a, int b){this->m_A = a;this->m_B = b;}//成員函數實現 + 號運算符重載(調用它的對象(this)+(p)?)Person operator+(const Person& p) {Person temp;temp.m_A = this->m_A + p.m_A;temp.m_B = this->m_B + p.m_B;return temp;}public:int m_A;int m_B; };//全局函數實現 + 號運算符重載 //Person operator+(const Person& p1, const Person& p2) { // Person temp(0, 0); // temp.m_A = p1.m_A + p2.m_A; // temp.m_B = p1.m_B + p2.m_B; // return temp; //}//運算符重載 可以發生函數重載 Person operator+(const Person& p2, int val) {Person temp;temp.m_A = p2.m_A + val;temp.m_B = p2.m_B + val;return temp; }void test() {Person p1(10, 10);Person p2(20, 20);//成員函數方式Person p3 = p2 + p1; //相當于 p2.operaor+(p1)cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;Person p4 = p3 + 10; //相當于 operator+(p3,10)cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;}int main() {test();system("pause");return 0; } mA:30 mB:30 mA:40 mB:40總結1:對于內置的數據類型的表達式的的運算符是不可能改變的
總結2:不要濫用運算符重載
4.5.2 左移運算符重載(輸出自定義數據類型)(ostream& operator<<)
https://www.bilibili.com/video/BV1et411b73Z?p=122
看了視頻教程才看明白,
作用:可以輸出自定義數據類型
#include<iostream> #include<string> using namespace std;class Person {friend ostream& operator<<(ostream& out, Person& p);public:Person(int a, int b){this->m_A = a;this->m_B = b;}//成員函數 實現不了 p << cout 不是我們想要的效果//void operator<<(Person& p){//}private:int m_A;int m_B; };//全局函數實現左移重載 //ostream對象只能有一個 ostream& operator<<(ostream& out, Person& p) {out << "a:" << p.m_A << " b:" << p.m_B;return out; }void test() {Person p1(10, 20);cout << p1 << "hello world" << endl; //鏈式編程 }int main() {test();system("pause");return 0; } a:10 b:20hello world總結:重載左移運算符配合友元可以實現輸出自定義數據類型
下面未檢測-------------------------------上面需再次看視頻,因為確實很多知識文檔里沒講到而視頻里講到了
4.5.3 遞增運算符重載
作用: 通過重載遞增運算符,實現自己的整型數據
class MyInteger {friend ostream& operator<<(ostream& out, MyInteger myint);public:MyInteger() {m_Num = 0;}//前置++MyInteger& operator++() {//先++m_Num++;//再返回return *this;}//后置++MyInteger operator++(int) {//先返回MyInteger temp = *this; //記錄當前本身的值,然后讓本身的值加1,但是返回的是以前的值,達到先返回后++;m_Num++;return temp;}private:int m_Num; };ostream& operator<<(ostream& out, MyInteger myint) {out << myint.m_Num;return out; }//前置++ 先++ 再返回 void test01() {MyInteger myInt;cout << ++myInt << endl;cout << myInt << endl; }//后置++ 先返回 再++ void test02() {MyInteger myInt;cout << myInt++ << endl;cout << myInt << endl; }int main() {test01();//test02();system("pause");return 0; }總結: 前置遞增返回引用,后置遞增返回值
4.5.4 賦值運算符重載(對屬性值進行拷貝)(這個用得多不多啊,搞得語法有點亂不是么?)
c++編譯器至少給一個類添加4個函數
如果類中有屬性指向堆區,做賦值操作時也會出現深淺拷貝問題
示例:
#include<iostream> #include<string> using namespace std;class Person { public:Person(int age){//將年齡數據開辟到堆區m_Age = new int(age);}//重載賦值運算符 Person& operator=(Person& p){//if(this->m_Age != NULL)if (m_Age != NULL){delete m_Age;m_Age = NULL;}//編譯器提供的代碼是淺拷貝//m_Age = p.m_Age;//提供深拷貝 解決淺拷貝的問題m_Age = new int(*p.m_Age);//返回自身return *this;}~Person(){if (m_Age != NULL){delete m_Age;m_Age = NULL;}}//年齡的指針int* m_Age;};void test01() {Person p1(18);Person p2(20);Person p3(30);p3 = p2 = p1; //賦值操作cout << "p1的年齡為:" << *p1.m_Age << endl;cout << "p2的年齡為:" << *p2.m_Age << endl;cout << "p3的年齡為:" << *p3.m_Age << endl; }int main() {test01();//int a = 10;//int b = 20;//int c = 30;//c = b = a;//cout << "a = " << a << endl;//cout << "b = " << b << endl;//cout << "c = " << c << endl;system("pause");return 0; }4.5.5 關系運算符重載(靈活是靈活,但是用普通函數也可以實現呀!)
作用:重載關系運算符,可以讓兩個自定義類型對象進行對比操作
示例:
#include<iostream> #include<string> using namespace std;class Person { public:Person(string name, int age){this->m_Name = name;this->m_Age = age;};bool operator==(Person& p){if (this->m_Name == p.m_Name && this->m_Age == p.m_Age){return true;}else{return false;}}bool operator!=(Person& p) //為啥不直接寫個equal函數?{if (this->m_Name == p.m_Name && this->m_Age == p.m_Age){return false;}else{return true;}}string m_Name;int m_Age; };void test01() {//int a = 0;//int b = 0;Person a("孫悟空", 18);Person b("孫悟空", 18);if (a == b){cout << "a和b相等" << endl;}else{cout << "a和b不相等" << endl;}if (a != b){cout << "a和b不相等" << endl;}else{cout << "a和b相等" << endl;} }int main() {test01();system("pause");return 0; } a和b相等 a和b相等4.5.6 函數調用運算符重載(仿函數)(如果要理解還得看視頻,沒看,先過了。。。)
- 函數調用運算符 () 也可以重載
- 由于重載后使用的方式非常像函數的調用,因此稱為仿函數
- 仿函數沒有固定寫法,非常靈活
示例:
#include<iostream> #include<string> using namespace std;class MyPrint { public:void operator()(string text){cout << text << endl;}}; void test01() {//重載的()操作符 也稱為仿函數MyPrint myFunc;myFunc("hello world"); }class MyAdd { public:int operator()(int v1, int v2){return v1 + v2;} };void test02() {MyAdd add;int ret = add(10, 10);cout << "ret = " << ret << endl;//匿名對象調用 cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl; }int main() {test01();test02();system("pause");return 0; } hello world ret = 20 MyAdd()(100,100) = 200總結
以上是生活随笔為你收集整理的【黑马程序员 C++教程从0到1入门编程】【笔记4】C++核心编程(类和对象——封装、权限、对象的初始化和清理、构造函数、析构函数、深拷贝、浅拷贝、初始化列表、友元friend、运算符重载)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++有的地方为什么要类内定义,类外实现
- 下一篇: C++ #pragma comment语