1 類和對象的基本概念
1.1 C和C 中struct區别
c語言struct隻有變量
c 語言struct 既有變量,也有函數
1.2 類的封裝
我們編寫程序的目的是為了解決現實中的問題,而這些問題的構成都是由各種事物組成,我們在計算機中要解決這種問題,首先要做就是要将這個問題的參與者:事和物抽象到計算機程序中,也就是用程序語言表示現實的事物。
那麼現在問題是如何用程序語言來表示現實事物?現實世界的事物所具有的共性就是每個事物都具有自身的屬性,一些自身具有的行為,所以如果我們能把事物的屬性和行為表示出來,那麼就可以抽象出來這個事物。
如果你想學C/C 可以私信小編“01”獲取素材資料以及開發工具和聽課權限哦!
比如我們要表示人這個對象,在c語言中,我們可以這麼表示:
typedef struct _Person{ char name[64]; int age; }Person; typedef struct _Aninal{ char name[64]; int age; int type; //動物種類 }Ainmal; void PersonEat(Person* person){ printf("%s在吃人吃的飯!\n",person->name); } void AnimalEat(Ainmal* animal){ printf("%s在吃動物吃的飯!\n", animal->name); } int main(){ Person person; strcpy(person.name, "小明"); person.age = 30; AnimalEat(&person); return EXIT_SUCCESS; }
定義一個結構體用來表示一個對象所包含的屬性,函數用來表示一個對象所具有的行為,這樣我們就表示出來一個事物,在c語言中,行為和屬性是分開的,也就是說吃飯這個屬性不屬于某類對象,而屬于所有的共同的數據,所以不單單是PeopleEat可以調用Person數據,AnimalEat也可以調用Person數據,那麼萬一調用錯誤,将會導緻問題發生。
從這個案例我們應該可以體會到,屬性和行為應該放在一起,一起表示一個具有屬性和行為的對象。
假如某對象的某項屬性不想被外界獲知,比如說漂亮女孩的年齡不想被其他人知道,那麼年齡這條屬性應該作為女孩自己知道的屬性;或者女孩的某些行為不想讓外界知道,隻需要自己知道就可以。那麼這種情況下,封裝應該再提供一種機制能夠給屬性和行為的訪問權限控制住。
所以說封裝特性包含兩個方面,一個是屬性和變量合成一個整體,一個是給屬性和函數增加訪問權限。
封裝
把變量(屬性)和函數(操作)合成一個整體,封裝在一個類中
對變量和函數進行訪問控制
訪問權限
在類的内部(作用域範圍内),沒有訪問權限之分,所有成員可以相互訪問
在類的外部(作用域範圍外),訪問權限才有意義:public,private,protected
在類的外部,隻有public修飾的成員才能被訪問,在沒有涉及繼承與派生時,private和protected是同等級的,外部不允許訪問
//封裝兩層含義 //1. 屬性和行為合成一個整體 //2. 訪問控制,現實事物本身有些屬性和行為是不對外開放 class Person{ //人具有的行為(函數) public: void Dese(){ cout << "我有錢,年輕,個子又高,就愛嘚瑟!" << endl;} //人的屬性(變量) public: int mTall; //多高,可以讓外人知道 protected: int mMoney; // 有多少錢,隻能兒子孫子知道 private: int mAge; //年齡,不想讓外人知道 }; int main(){ Person p; p.mTall = 220; //p.mMoney 保護成員外部無法訪問 //p.mAge 私有成員外部無法訪問 p.Dese(); return EXIT_SUCCESS; }
[struct和class的區别?]
.
class默認訪問權限為private,struct默認訪問權限為public.
class A{ int mAge; }; struct B{ int mAge; }; void test(){ A a; B b; //a.mAge; //無法訪問私有成員 b.mAge; //可正常外部訪問 }
1.3 将成員變量設置為private
1. 可賦予客戶端訪問數據的一緻性。
如果成員變量不是public,客戶端唯一能夠訪問對象的方法就是通過成員函數。如果類中所有public權限的成員都是函數,客戶在訪問類成員時隻會默認訪問函數,不需要考慮訪問的成員需不需要添加(),這就省下了許多搔首弄耳的時間。
2. 可細微劃分訪問控制。
使用成員函數可使得我們對變量的控制處理更加精細。如果我們讓所有的成員變量為public,每個人都可以讀寫它。如果我們設置為private,我們可以實現“不準訪問”、“隻讀訪問”、“讀寫訪問”,甚至你可以寫出“隻寫訪問”。
class AccessLevels{ public: //對隻讀屬性進行隻讀訪問 int getReadOnly(){ return readOnly; } //對讀寫屬性進行讀寫訪問 void setReadWrite(int val){ readWrite = val; } int getReadWrite(){ return readWrite; } //對隻寫屬性進行隻寫訪問 void setWriteOnly(int val){ writeOnly = val; } private: int readOnly; //對外隻讀訪問 int noAccess; //外部不可訪問 int readWrite; //讀寫訪問 int writeOnly; //隻寫訪問 };
1.3課堂練習
請設計一個Person類,Person類具有name和age屬性,提供初始化函數(Init),并提供對name和age的讀寫函數(set,get),但必須确保age的賦值在有效範圍内(0-100),超出有效範圍,則拒絕賦值,并提供方法輸出姓名和年齡.(10分鐘)
2 面向對象程序設計案例
2.1 設計立方體類
設計立方體類(Cube),求出立方體的面積( 2*a*b 2*a*c 2*b*c )和體積( a * b * c),分别用全局函數和成員函數判斷兩個立方體是否相等。
//立方體類 class Cub{ public: void setL(int l){ mL = l; } void setW(int w){ mW = w; } void setH(int h){ mH = h; } int getL(){ return mL; } int getW(){ return mW; } int getH(){ return mH; } //立方體面積 int caculateS(){ return (mL*mW mL*mH mW*mH) * 2; } //立方體體積 int caculateV(){ return mL * mW * mH; } //成員方法 bool CubCompare(Cub& c){ if (getL() == c.getL() && getW() == c.getW() && getH() == c.getH()){ return true; } return false; } private: int mL; //長 int mW; //寬 int mH; //高 }; //比較兩個立方體是否相等 bool CubCompare(Cub& c1, Cub& c2){ if (c1.getL() == c2.getL() && c1.getW() == c2.getW() && c1.getH() == c2.getH()){ return true; } return false; } void test(){ Cub c1, c2; c1.setL(10); c1.setW(20); c1.setH(30); c2.setL(20); c2.setW(20); c2.setH(30); cout << "c1面積:" << c1.caculateS() << " 體積:" << c1.caculateV() << endl; cout << "c2面積:" << c2.caculateS() << " 體積:" << c2.caculateV() << endl; //比較兩個立方體是否相等 if (CubCompare(c1, c2)){ cout << "c1和c2相等!" << endl; } else{ cout << "c1和c2不相等!" << endl; } if (c1.CubCompare(c2)){ cout << "c1和c2相等!" << endl; } else{ cout << "c1和c2不相等!" << endl; } }
2.2 點和圓的關系
設計一個圓形類(AdvCircle),和一個點類(Point),計算點和圓的關系。
假如圓心坐标為x0, y0, 半徑為r,點的坐标為x1, y1:
1)點在圓上:(x1-x0)*(x1-x0) (y1-y0)*(y1-y0) == r*r
2)點在圓内:(x1-x0)*(x1-x0) (y1-y0)*(y1-y0) < r*r
3)點在圓外:(x1-x0)*(x1-x0) (y1-y0)*(y1-y0) > r*r
//點類 class Point{ public: void setX(int x){ mX = x; } void setY(int y){ mY = y; } int getX(){ return mX; } int getY(){ return mY; } private: int mX; int mY; }; //圓類 class Circle{ public: void setP(int x,int y){ mP.setX(x); mP.setY(y); } void setR(int r){ mR = r; } Point& getP(){ return mP; } int getR(){ return mR; } //判斷點和圓的關系 void IsPointInCircle(Point& point){ int distance = (point.getX() - mP.getX()) * (point.getX() - mP.getX()) (point.getY() - mP.getY()) * (point.getY() - mP.getY()); int radius = mR * mR; if (distance < radius){ cout << "Point(" << point.getX() << "," << point.getY() << ")在圓内!" << endl; } else if (distance > radius){ cout << "Point(" << point.getX() << "," << point.getY() << ")在圓外!" << endl; } else{ cout << "Point(" << point.getX() << "," << point.getY() << ")在圓上!" << endl; } } private: Point mP; //圓心 int mR; //半徑 }; void test(){ //實例化圓對象 Circle circle; circle.setP(20, 20); circle.setR(5); //實例化點對象 Point point; point.setX(25); point.setY(20); circle.IsPointInCircle(point); }
3 對象的構造和析構
3.1 初始化和清理
我們大家在購買一台電腦或者手機,或者其他的産品,這些産品都有一個初始設置,也就是這些産品對被創建的時候會有一個基礎屬性值。那麼随着我們使用手機和電腦的時間越來越久,那麼電腦和手機會慢慢被我們手動創建很多文件數據,某一天我們不用手機或電腦了,那麼我們應該将電腦或手機中我們增加的數據删除掉,保護自己的信息數據。
從這樣的過程中,我們體會一下,所有的事物在起初的時候都應該有個初始狀态,當這個事物完成其使命時,應該及時清除外界作用于上面的一些信息數據。
那麼我們c 中OO思想也是來源于現實,是對現實事物的抽象模拟,具體來說,當我們創建對象的時候,這個對象應該有一個初始狀态,當對象銷毀之前應該銷毀自己創建的一些數據。
對象的初始化和清理也是兩個非常重要的安全問題,一個對象或者變量沒有初始時,對其使用後果是未知,同樣的使用完一個變量,沒有及時清理,也會造成一定的安全問題。c 為了給我們提供這種問題的解決方案,構造函數和析構函數,這兩個函數将會被編譯器自動調用,完成對象初始化和對象清理工作。
無論你是否喜歡,對象的初始化和清理工作是編譯器強制我們要做的事情,即使你不提供初始化操作和清理操作,編譯器也會給你增加默認的操作,隻是這個默認初始化操作不會做任何事,所以編寫類就應該順便提供初始化函數。
為什麼初始化操作是自動調用而不是手動調用?既然是必須操作,那麼自動調用會更好,如果靠程序員自覺,那麼就會存在遺漏初始化的情況出現。
3.1 構造函數和析構函數
構造函數主要作用在于創建對象時為對象的成員屬性賦值,構造函數由編譯器自動調用,無須手動調用。
析構函數主要用于對象銷毀前系統自動調用,執行一些清理工作。
構造函數語法:
構造函數函數名和類名相同,沒有返回值,不能有void,但可以有參數。
ClassName(){}
析構函數語法:
析構函數函數名是在類名前面加”~”組成,沒有返回值,不能有void,不能有參數,不能重載。
class Person{ public: Person(){ cout << "構造函數調用!" << endl; pName = (char*)malloc(sizeof("John")); strcpy(pName, "John"); mTall = 150; mMoney = 100; } ~Person(){ cout << "析構函數調用!" << endl; if (pName != NULL){ free(pName); pName = NULL; } } public: char* pName; int mTall; int mMoney; }; void test(){ Person person; cout << person.pName << person.mTall << person.mMoney << endl; }
3.1 構造函數的分類及調用
class Person{ public: Person(){ cout << "no param constructor!" << endl; mAge = 0; } //有參構造函數 Person(int age){ cout << "1 param constructor!" << endl; mAge = age; } //拷貝構造函數(複制構造函數) 使用另一個對象初始化本對象 Person(const Person& person){ cout << "copy constructor!" << endl; mAge = person.mAge; } //打印年齡 void PrintPerson(){ cout << "Age:" << mAge << endl; } private: int mAge; }; //1. 無參構造調用方式 void test01(){ //調用無參構造函數 Person person1; person1.PrintPerson(); //無參構造函數錯誤調用方式 //Person person2(); //person2.PrintPerson(); } //2. 調用有參構造函數 void test02(){ //第一種 括号法,最常用 Person person01(100); person01.PrintPerson(); //調用拷貝構造函數 Person person02(person01); person02.PrintPerson(); //第二種 匿名對象(顯示調用構造函數) Person(200); //匿名對象,沒有名字的對象 Person person03 = Person(300); person03.PrintPerson(); //注意: 使用匿名對象初始化判斷調用哪一個構造函數,要看匿名對象的參數類型 Person person06(Person(400)); //等價于 Person person06 = Person(400); person06.PrintPerson(); //第三種 =号法 隐式轉換 Person person04 = 100; //Person person04 = Person(100) person04.PrintPerson(); //調用拷貝構造 Person person05 = person04; //Person person05 = Person(person04) person05.PrintPerson(); }
b為A的實例化對象,A a = A(b) 和 A(b)的區别?
.
當A(b) 有變量來接的時候,那麼編譯器認為他是一個匿名對象,當沒有變量來接的時候,編譯器認為A(b) 等價于 A b.
**注意:**不能調用拷貝構造函數去初始化匿名對象,也就是說以下代碼不正确:
class Teacher{ public: Teacher(){ cout << "默認構造函數!" << endl; } Teacher(const Teacher& teacher){ cout << "拷貝構造函數!" << endl; } public: int mAge; }; void test(){ Teacher t1; //error C2086:“Teacher t1”: 重定義 Teacher(t1); //此時等價于 Teacher t1; }
3.2 拷貝構造函數的調用時機
class Person{ public: Person(){ cout << "no param contructor!" << endl; mAge = 10; } Person(int age){ cout << "param constructor!" << endl; mAge = age; } Person(const Person& person){ cout << "copy constructor!" << endl; mAge = person.mAge; } ~Person(){ cout << "destructor!" << endl; } public: int mAge; }; //1. 舊對象初始化新對象 void test01(){ Person p(10); Person p1(p); Person p2 = Person(p); Person p3 = p; // 相當于Person p2 = Person(p); } //2. 傳遞的參數是普通對象,函數參數也是普通對象,傳遞将會調用拷貝構造 void doBussiness(Person p){} void test02(){ Person p(10); doBussiness(p); } //3. 函數返回局部對象 Person MyBusiness(){ Person p(10); cout << "局部p:" << (int*)&p << endl; return p; } void test03(){ //vs release、qt下沒有調用拷貝構造函數 //vs debug下調用一次拷貝構造函數 Person p = MyBusiness(); cout << "局部p:" << (int*)&p << endl; }
Test03結果說明:
編譯器存在一種對返回值的優化技術,RVO(Return Value Optimization).在vs debug模式下并沒有進行這種優化,所以函數MyBusiness中創建p對象,調用了一次構造函數,當編譯器發現你要返回這個局部的對象時,編譯器通過調用拷貝構造生成一個臨時Person對象返回,然後調用p的析構函數。
我們從常理來分析的話,這個匿名對象和這個局部的p對象是相同的兩個對象,那麼如果能直接返回p對象,就會省去一個拷貝構造和一個析構函數的開銷,在程序中一個對象的拷貝也是非常耗時的,如果減少這種拷貝和析構的次數,那麼從另一個角度來說,也是編譯器對程序執行效率上進行了優化。
所以在這裡,編譯器偷偷幫我們做了一層優化:
當我們這樣去調用: Person p = MyBusiness();
編譯器偷偷将我們的代碼更改為:
void MyBussiness(Person& _result){ _result.X:X(); //調用Person默認拷貝構造函數 //.....對_result進行處理 return; } int main(){ Person p; //這裡隻分配空間,不初始化 MyBussiness(p); }
3.3 構造函數調用規則
默認情況下,c 編譯器至少為我們寫的類增加3個函數
1.默認構造函數(無參,函數體為空)
2.默認析構函數(無參,函數體為空)
3.默認拷貝構造函數,對類中非靜态成員屬性簡單值拷貝
如果用戶定義拷貝構造函數,c 不會再提供任何默認構造函數
如果用戶定義了普通構造(非拷貝),c 不在提供默認無參構造,但是會提供默認拷貝構造
3.4 深拷貝和淺拷貝
3.4.1 淺拷貝
同一類型的對象之間可以賦值,使得兩個對象的成員變量的值相同,兩個對象仍然是獨立的兩個對象,這種情況被稱為淺拷貝.
一般情況下,淺拷貝沒有任何副作用,但是當類中有指針,并且指針指向動态分配的内存空間,析構函數做了動态内存釋放的處理,會導緻内存問題。
3.4.2 深拷貝
當類中有指針,并且此指針有動态分配空間,析構函數做了釋放處理,往往需要自定義拷貝構造函數,自行給指針動态分配空間,深拷貝。
class Person{ public: Person(char* name,int age){ pName = (char*)malloc(strlen(name) 1); strcpy(pName,name); mAge = age; } //增加拷貝構造函數 Person(const Person& person){ pName = (char*)malloc(strlen(person.pName) 1); strcpy(pName, person.pName); mAge = person.mAge; } ~Person(){ if (pName != NULL){ free(pName); } } private: char* pName; int mAge; }; void test(){ Person p1("Edward",30); //用對象p1初始化對象p2,調用c 提供的默認拷貝構造函數 Person p2 = p1; }
3.4.2 類對象作為成員
在類中定義的數據成員一般都是基本的數據類型。但是類中的成員也可以是對象,叫做對象成員。
C 中對對象的初始化是非常重要的操作,當創建一個對象的時候,c 編譯器必須确保調用了所有子對象的構造函數。如果所有的子對象有默認構造函數,編譯器可以自動調用他們。但是如果子對象沒有默認的構造函數,或者想指定調用某個構造函數怎麼辦?
那麼是否可以在類的構造函數直接調用子類的屬性完成初始化呢?但是如果子類的成員屬性是私有的,我們是沒有辦法訪問并完成初始化的。
解決辦法非常簡單:對于子類調用構造函數,c 為此提供了專門的語法,即構造函數初始化列表。
當調用構造函數時,首先按各對象成員在類定義中的順序(和參數列表的順序無關) 依次調用它們的構造函數,對這些對象初始化,最後再調用本身的函數體。也就是說,先調用對象成員的構造函數,再調用本身的構造函數。
析構函數和構造函數調用順序相反,先構造,後析構。
//汽車類 class Car{ public: Car(){ cout << "Car 默認構造函數!" << endl; mName = "大衆汽車"; } Car(string name){ cout << "Car 帶參數構造函數!" << endl; mName = name; } ~Car(){ cout << "Car 析構函數!" << endl; } public: string mName; }; //拖拉機 class Tractor{ public: Tractor(){ cout << "Tractor 默認構造函數!" << endl; mName = "爬土坡專用拖拉機"; } Tractor(string name){ cout << "Tractor 帶參數構造函數!" << endl; mName = name; } ~Tractor(){ cout << "Tractor 析構函數!" << endl; } public: string mName; }; //人類 class Person{ public: #if 1 //類mCar不存在合适的構造函數 Person(string name){ mName = name; } #else //初始化列表可以指定調用構造函數 Person(string carName, string tracName, string name) : mTractor(tracName), mCar(carName), mName(name){ cout << "Person 構造函數!" << endl; } #endif void GoWorkByCar(){ cout << mName << "開着" << mCar.mName << "去上班!" << endl; } void GoWorkByTractor(){ cout << mName << "開着" << mTractor.mName << "去上班!" << endl; } ~Person(){ cout << "Person 析構函數!" << endl; } private: string mName; Car mCar; Tractor mTractor; }; void test(){ //Person person("寶馬", "東風拖拉機", "趙四"); Person person("劉能"); person.GoWorkByCar(); person.GoWorkByTractor(); }
3.5 explicit關鍵字
c 提供了關鍵字explicit,禁止通過構造函數進行的隐式轉換。聲明為explicit的構造函數不能在隐式轉換中使用。
[explicit注意]
explicit用于修飾構造函數,防止隐式轉化。
是針對單參數的構造函數(或者除了第一個參數外其餘參數都有默認值的多參構造)而言。
class MyString{ public: explicit MyString(int n){ cout << "MyString(int n)!" << endl; } MyString(const char* str){ cout << "MyString(const char* str)" << endl; } }; int main(){ //給字符串賦值?還是初始化? //MyString str1 = 1; MyString str2(10); //寓意非常明确,給字符串賦值 MyString str3 = "abcd"; MyString str4("abcd"); return EXIT_SUCCESS; }
3.6 動态對象創建
當我們創建數組的時候,總是需要提前預定數組的長度,然後編譯器分配預定長度的數組空間,在使用數組的時,會有這樣的問題,數組也許空間太大了,浪費空間,也許空間不足,所以對于數組來講,如果能根據需要來分配空間大小再好不過。
所以動态的意思意味着不确定性。
為了解決這個普遍的編程問題,在運行中可以創建和銷毀對象是最基本的要求。當然c早就提供了動态内存分配(dynamic memory allocation),函數malloc和free可以在運行時從堆中分配存儲單元。
然而這些函數在c 中不能很好的運行,因為它不能幫我們完成對象的初始化工作。
3.6.1 對象創建
當創建一個c 對象時會發生兩件事:
為對象分配内存
調用構造函數來初始化那塊内存
第一步我們能保證實現,需要我們确保第二步一定能發生。c 強迫我們這麼做是因為使用未初始化的對象是程序出錯的一個重要原因。
3.6.2 C動态分配内存方法
為了在運行時動态分配内存,c在他的标準庫中提供了一些函數,malloc以及它的變種calloc和realloc,釋放内存的free,這些函數是有效的、但是原始的,需要程序員理解和小心使用。為了使用c的動态内存分配函數在堆上創建一個類的實例,我們必須這樣做:
class Person{ public: Person(){ mAge = 20; pName = (char*)malloc(strlen("john") 1); strcpy(pName, "john"); } void Init(){ mAge = 20; pName = (char*)malloc(strlen("john") 1); strcpy(pName, "john"); } void Clean(){ if (pName != NULL){ free(pName); } } public: int mAge; char* pName; }; int main(){ //分配内存 Person* person = (Person*)malloc(sizeof(Person)); if(person == NULL){ return 0; } //調用初始化函數 person->Init(); //清理對象 person->Clean(); //釋放person對象 free(person); return EXIT_SUCCESS; }
問題:
程序員必須确定對象的長度。
malloc返回一個void指針,c 不允許将void賦值給其他任何指針,必須強轉。
malloc可能申請内存失敗,所以必須判斷返回值來确保内存分配成功。
用戶在使用對象之前必須記住對他初始化,構造函數不能顯示調用初始化(構造函數是由編譯器調用),用戶有可能忘記調用初始化函數。
c的動态内存分配函數太複雜,容易令人混淆,是不可接受的,c 中我們推薦使用運算符new 和 delete.
3.6.3 new operator
C 中解決動态内存分配的方案是把創建一個對象所需要的操作都結合在一個稱為new的運算符裡。當用new創建一個對象時,它就在堆裡為對象分配内存并調用構造函數完成初始化。
Person* person = new Person; 相當于: Person* person = (Person*)malloc(sizeof(Person)); if(person == NULL){ return 0; } person->Init();
New操作符能确定在調用構造函數初始化之前内存分配是成功的,所有不用顯式确定調用是否成功。
現在我們發現在堆裡創建對象的過程變得簡單了,隻需要一個簡單的表達式,它帶有内置的長度計算、類型轉換和安全檢查。這樣在堆創建一個對象和在棧裡創建對象一樣簡單。
3.6.4 delete operator
new表達式的反面是delete表達式。delete表達式先調用析構函數,然後釋放内存。正如new表達式返回一個指向對象的指針一樣,delete需要一個對象的地址。
delete隻适用于由new創建的對象。
如果使用一個由malloc或者calloc或者realloc創建的對象使用delete,這個行為是未定義的。因為大多數new和delete的實現機制都使用了malloc和free,所以很可能沒有調用析構函數就釋放了内存。
如果正在删除的對象的指針是NULL,将不發生任何事,因此建議在删除指針後,立即把指針賦值為NULL,以免對它删除兩次,對一些對象删除兩次可能會産生某些問題。
class Person{ public: Person(){ cout << "無參構造函數!" << endl; pName = (char*)malloc(strlen("undefined") 1); strcpy(pName, "undefined"); mAge = 0; } Person(char* name, int age){ cout << "有參構造函數!" << endl; pName = (char*)malloc(strlen(name) 1); strcpy(pName, name); mAge = age; } void ShowPerson(){ cout << "Name:" << pName << " Age:" << mAge << endl; } ~Person(){ cout << "析構函數!" << endl; if (pName != NULL){ delete pName; pName = NULL; } } public: char* pName; int mAge; }; void test(){ Person* person1 = new Person; Person* person2 = new Person("John",33); person1->ShowPerson(); person2->ShowPerson(); delete person1; delete person2; }
3.6.5 用于數組的new和delete
使用new和delete在堆上創建數組非常容易。
//創建字符數組 char* pStr = new char[100]; //創建整型數組 int* pArr1 = new int[100]; //創建整型數組并初始化 int* pArr2 = new int[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //釋放數組内存 delete[] pStr; delete[] pArr1; delete[] pArr2;
當創建一個對象數組的時候,必須對數組中的每一個對象調用構造函數,除了在棧上可以聚合初始化,必須提供一個默認的構造函數。
class Person{ public: Person(){ pName = (char*)malloc(strlen("undefined") 1); strcpy(pName, "undefined"); mAge = 0; } Person(char* name, int age){ pName = (char*)malloc(sizeof(name)); strcpy(pName, name); mAge = age; } ~Person(){ if (pName != NULL){ delete pName; } } public: char* pName; int mAge; }; void test(){ //棧聚合初始化 Person person[] = { Person("john", 20), Person("Smith", 22) }; cout << person[1].pName << endl; //創建堆上對象數組必須提供構造函數 Person* workers = new Person[20]; }
3.6.6 delete void*可能會出錯
如果對一個void*指針執行delete操作,這将可能成為一個程序錯誤,除非指針指向的内容是非常簡單的,因為它将不執行析構函數.以下代碼未調用析構函數,導緻可用内存減少。
class Person{ public: Person(char* name, int age){ pName = (char*)malloc(sizeof(name)); strcpy(pName,name); mAge = age; } ~Person(){ if (pName != NULL){ delete pName; } } public: char* pName; int mAge; }; void test(){ void* person = new Person("john",20); delete person; }
問題:
malloc、free和new、delete可以混搭使用嗎?也就是說malloc分配的内存,可以調用delete嗎?通過new創建的對象,可以調用free來釋放嗎?
3.6.7 使用new和delete采用相同形式
Person* person = new Person[10]; delete person;
以上代碼有什麼問題嗎?(vs下直接中斷、qt下析構函數調用一次)
使用了new也搭配使用了delete,問題在于Person有10個對象,那麼其他9個對象可能沒有調用析構函數,也就是說其他9個對象可能删除不完全,因為它們的析構函數沒有被調用。
我們現在清楚使用new的時候發生了兩件事: 一、分配内存;二、調用構造函數,那麼調用delete的時候也有兩件事:一、析構函數;二、釋放内存。
那麼剛才我們那段代碼最大的問題在于:person指針指向的内存中到底有多少個對象,因為這個決定應該有多少個析構函數應該被調用。換句話說,person指針指向的是一個單一的對象還是一個數組對象,由于單一對象和數組對象的内存布局是不同的。更明确的說,數組所用的内存通常還包括“數組大小記錄”,使得delete的時候知道應該調用幾次析構函數。單一對象的話就沒有這個記錄。單一對象和數組對象的内存布局可理解為下圖:
本圖隻是為了說明,編譯器不一定如此實現,但是很多編譯器是這樣做的。
當我們使用一個delete的時候,我們必須讓delete知道指針指向的内存空間中是否存在一個“數組大小記錄”的辦法就是我們告訴它。當我們使用delete[],那麼delete就知道是一個對象數組,從而清楚應該調用幾次析構函數。
結論:
如果在new表達式中使用[],必須在相應的delete表達式中也使用[].如果在new表達式中不使用[], 一定不要在相應的delete表達式中使用[].
3.7 靜态成員
在類定義中,它的成員(包括成員變量和成員函數),這些成員可以用關鍵字static聲明為靜态的,稱為靜态成員。
不管這個類創建了多少個對象,靜态成員隻有一個拷貝,這個拷貝被所有屬于這個類的對象共享。
3.7.1 靜态成員變量
在一個類中,若将一個成員變量聲明為static,這種成員稱為靜态成員變量。與一般的數據成員不同,無論建立了多少個對象,都隻有一個靜态數據的拷貝。靜态成員變量,屬于某個類,所有對象共享。
靜态變量,是在編譯階段就分配空間,對象還沒有創建時,就已經分配空間。
靜态成員變量必須在類中聲明,在類外定義。
靜态數據成員不屬于某個對象,在為對象分配空間中不包括靜态成員所占空間。
class Person{ public: //類的靜态成員屬性 static int sNum; private: static int sOther; }; //類外初始化,初始化時不加static int Person::sNum = 0; int Person::sOther = 0; int main(){ //1. 通過類名直接訪問 Person::sNum = 100; cout << "Person::sNum:" << Person::sNum << endl; //2. 通過對象訪問 Person p1, p2; p1.sNum = 200; cout << "p1.sNum:" << p1.sNum << endl; cout << "p2.sNum:" << p2.sNum << endl; //3. 靜态成員也有訪問權限,類外不能訪問私有成員 //cout << "Person::sOther:" << Person::sOther << endl; Person p3; //cout << "p3.sOther:" << p3.sOther << endl; system("pause"); return EXIT_SUCCESS; }
3.7.2 靜态成員函數
在類定義中,前面有static說明的成員函數稱為靜态成員函數。靜态成員函數使用方式和靜态變量一樣,同樣在對象沒有創建前,即可通過類名調用。靜态成員函數主要為了訪問靜态變量,但是,不能訪問普通成員變量。
靜态成員函數的意義,不在于信息共享,數據溝通,而在于管理靜态數據成員,完成對靜态數據成員的封裝。
靜态成員函數隻能訪問靜态變量,不能訪問普通成員變量
靜态成員函數的使用和靜态成員變量一樣
靜态成員函數也有訪問權限
普通成員函數可訪問靜态成員變量、也可以訪問非經常成員變量
class Person{ public: //普通成員函數可以訪問static和non-static成員屬性 void changeParam1(int param){ mParam = param; sNum = param; } //靜态成員函數隻能訪問static成員屬性 static void changeParam2(int param){ //mParam = param; //無法訪問 sNum = param; } private: static void changeParam3(int param){ //mParam = param; //無法訪問 sNum = param; } public: int mParam; static int sNum; }; //靜态成員屬性類外初始化 int Person::sNum = 0; int main(){ //1. 類名直接調用 Person::changeParam2(100); //2. 通過對象調用 Person p; p.changeParam2(200); //3. 靜态成員函數也有訪問權限 //Person::changeParam3(100); //類外無法訪問私有靜态成員函數 //Person p1; //p1.changeParam3(200); return EXIT_SUCCESS; }
3.7.3 const靜态成員屬性
如果一個類的成員,既要實現共享,又要實現不可改變,那就用 static const 修飾。
定義靜态const數據成員時,最好在類内部初始化。
class Person{ public: //static const int mShare = 10; const static int mShare = 10; //隻讀區,不可修改 }; int main(){ cout << Person::mShare << endl; //Person::mShare = 20; return EXIT_SUCCESS; }
3.7.4 靜态成員實現單例模式
單例模式是一種常用的軟件設計模式。在它的核心結構中隻包含一個被稱為單例的特殊類。通過單例模式可以保證系統中一個類隻有一個實例而且該實例易于外界訪問,從而方便對實例個數的控制并節約系統資源。如果希望在系統中某個類的對象隻能存在一個,單例模式是最好的解決方案。
**Singleton(單例):**在單例類的内部實現隻生成一個實例,同時它提供一個靜态的getInstance()工廠方法,讓客戶可以訪問它的唯一實例;為了防止在外部對其實例化,将其默認構造函數和拷貝構造函數設計為私有;在單例類内部定義了一個Singleton類型的靜态對象,作為外部共享的唯一實例。
用單例模式,模拟公司員工使用打印機場景,打印機可以打印員工要輸出的内容,并且可以累積打印機使用次數。
class Printer{ public: static Printer* getInstance(){ return pPrinter;} void PrintText(string text){ cout << "打印内容:" << text << endl; cout << "已打印次數:" << mTimes << endl; cout << "--------------" << endl; mTimes ; } private: Printer(){ mTimes = 0; } Printer(const Printer&){} private: static Printer* pPrinter; int mTimes; }; Printer* Printer::pPrinter = new Printer; void test(){ Printer* printer = Printer::getInstance(); printer->PrintText("離職報告!"); printer->PrintText("入職合同!"); printer->PrintText("提交代碼!"); }
4 C 面向對象模型初探
4.4.1 成員變量和函數的存儲
在c語言中,“分開來聲明的,也就是說,語言本身并沒有支持“數據”和“函數”之間的關聯性我們把這種程序方法稱為“程序性的”,由一組“分布在各個以功能為導航的函數中”的算法驅動,它們處理的是共同的外部數據。
c 實現了“封裝”,那麼數據(成員屬性)和操作(成員函數)是什麼樣的呢?
“數據”和“處理數據的操作(函數)”是分開存儲的。
c 中的非靜态數據成員直接内含在類對象中,就像c struct一樣。
成員函數(member function)雖然内含在class聲明之内,卻不出現在對象中。
每一個非内聯成員函數(non-inline member function)隻會誕生一份函數實例.
class MyClass01{ public: int mA; }; class MyClass02{ public: int mA; static int sB; }; class MyClass03{ public: void printMyClass(){ cout << "hello world!" << endl; } public: int mA; static int sB; }; class MyClass04{ public: void printMyClass(){ cout << "hello world!" << endl; } static void ShowMyClass(){ cout << "hello world!" << endl; } public: int mA; static int sB; }; int main(){ MyClass01 mclass01; MyClass02 mclass02; MyClass03 mclass03; MyClass04 mclass04; cout << "MyClass01:" << sizeof(mclass01) << endl; //4 //靜态數據成員并不保存在類對象中 cout << "MyClass02:" << sizeof(mclass02) << endl; //4 //非靜态成員函數不保存在類對象中 cout << "MyClass03:" << sizeof(mclass03) << endl; //4 //靜态成員函數也不保存在類對象中 cout << "MyClass04:" << sizeof(mclass04) << endl; //4 return EXIT_SUCCESS; }
通過上面的案例,我們可以的得出:C 類對象中的變量和函數是分開存儲。
4.2 this指針
4.2.1 this指針工作原理
通過上例我們知道,c 的數據和操作也是分開存儲,并且每一個非内聯成員函數(non-inline member function)隻會誕生一份函數實例,也就是說多個同類型的對象會共用一塊代碼
那麼問題是:這一塊代碼是如何區分那個對象調用自己的呢?
c 通過提供特殊的對象指針,this指針,解決上述問題。This指針指向被調用的成員函數所屬的對象。
c 規定,this指針是隐含在對象成員函數内的一種指針。當一個對象被創建後,它的每一個成員函數都含有一個系統自動生成的隐含指針this,用以保存這個對象的地址,也就是說雖然我們沒有寫上this指針,編譯器在編譯的時候也是會加上的。因此this也稱為“指向本對象的指針”,this指針并不是對象的一部分,不會影響sizeof(對象)的結果。
this指針是C 實現封裝的一種機制,它将對象和該對象調用的成員函數連接在一起,在外部看來,每一個對象都擁有自己的函數成員。一般情況下,并不寫this,而是讓系統進行默認設置。
this指針永遠指向當前對象。
成員函數通過this指針即可知道操作的是那個對象的數據。This指針是一種隐含指針,它隐含于每個類的非靜态成員函數中。This指針無需定義,直接使用即可。
注意:靜态成員函數内部沒有this指針,靜态成員函數不能操作非靜态成員變量。
c 編譯器對普通成員函數的内部處理
4.2.2 this指針的使用
class Person{ public: //1. 當形參名和成員變量名一樣時,this指針可用來區分 Person(string name,int age){ //name = name; //age = age; //輸出錯誤 this->name = name; this->age = age; } //2. 返回對象本身的引用 //重載賦值操作符 //其實也是兩個參數,其中隐藏了一個this指針 Person PersonPlusPerson(Person& person){ string newname = this->name person.name; int newage = this->age person.age; Person newperson(newname, newage); return newperson; } void ShowPerson(){ cout << "Name:" << name << " Age:" << age << endl; } public: string name; int age; }; //3. 成員函數和全局函數(Perosn對象相加) Person PersonPlusPerson(Person& p1,Person& p2){ string newname = p1.name p2.name; int newage = p1.age p2.age; Person newperson(newname,newage); return newperson; } int main(){ Person person("John",100); person.ShowPerson(); cout << "---------" << endl; Person person1("John",20); Person person2("001", 10); //1.全局函數實現兩個對象相加 Person person3 = PersonPlusPerson(person1, person2); person1.ShowPerson(); person2.ShowPerson(); person3.ShowPerson(); //2. 成員函數實現兩個對象相加 Person person4 = person1.PersonPlusPerson(person2); person4.ShowPerson(); system("pause"); return EXIT_SUCCESS; }
4.2.3 const修飾成員函數
//const修飾成員函數 class Person{ public: Person(){ this->mAge = 0; this->mID = 0; } //在函數括号後面加上const,修飾成員變量不可修改,除了mutable變量 void sonmeOperate() const{ //this->mAge = 200; //mAge不可修改 this->mID = 10; } void ShowPerson(){ cout << "ID:" << mID << " mAge:" << mAge << endl; } private: int mAge; mutable int mID; }; int main(){ Person person; person.sonmeOperate(); person.ShowPerson(); system("pause"); return EXIT_SUCCESS; }
4.4.2.4 const修飾對象(常對象)
class Person{ public: Person(){ this->mAge = 0; this->mID = 0; } void ChangePerson() const{ mAge = 100; mID = 100; } void ShowPerson(){ this->mAge = 1000; cout << "ID:" << this->mID << " Age:" << this->mAge << endl; } public: int mAge; mutable int mID; }; void test(){ const Person person; //1. 可訪問數據成員 cout << "Age:" << person.mAge << endl; //person.mAge = 300; //不可修改 person.mID = 1001; //但是可以修改mutable修飾的成員變量 //2. 隻能訪問const修飾的函數 //person.ShowPerson(); person.ChangePerson(); }
5 友元
類的主要特點之一是數據隐藏,即類的私有成員無法在類的外部(作用域之外)訪問。但是,有時候需要在類的外部訪問類的私有成員,怎麼辦?
解決方法是使用友元函數,友元函數是一種特權函數,c 允許這個特權函數訪問私有成員。這一點從現實生活中也可以很好的理解:
比如你的家,有客廳,有你的卧室,那麼你的客廳是Public的,所有來的客人都可以進去,但是你的卧室是私有的,也就是說隻有你能進去,但是呢,你也可以允許你的閨蜜好基友進去。
程序員可以把一個全局函數、某個類中的成員函數、甚至整個類聲明為友元。
5.1 友元語法
friend關鍵字隻出現在聲明處
其他類、類成員函數、全局函數都可聲明為友元
友元函數不是類的成員,不帶this指針
友元函數可訪問對象任意成員屬性,包括私有屬性
class Building; //友元類 class MyFriend{ public: //友元成員函數 void LookAtBedRoom(Building& building); void PlayInBedRoom(Building& building); }; class Building{ //全局函數做友元函數 friend void CleanBedRoom(Building& building); #if 0 //成員函數做友元函數 friend void MyFriend::LookAtBedRoom(Building& building); friend void MyFriend::PlayInBedRoom(Building& building); #else //友元類 friend class MyFriend; #endif public: Building(); public: string mSittingRoom; private: string mBedroom; }; void MyFriend::LookAtBedRoom(Building& building){ cout << "我的朋友參觀" << building.mBedroom << endl; } void MyFriend::PlayInBedRoom(Building& building){ cout << "我的朋友玩耍在" << building.mBedroom << endl; } //友元全局函數 void CleanBedRoom(Building& building){ cout << "友元全局函數訪問" << building.mBedroom << endl; } Building::Building(){ this->mSittingRoom = "客廳"; this->mBedroom = "卧室"; } int main(){ Building building; MyFriend myfriend; CleanBedRoom(building); myfriend.LookAtBedRoom(building); myfriend.PlayInBedRoom(building); system("pause"); return EXIT_SUCCESS; }
友元類注意
1.友元關系不能被繼承。
2.友元關系是單向的,類A是類B的朋友,但類B不一定是類A的朋友。
3.友元關系不具有傳遞性。類B是類A的朋友,類C是類B的朋友,但類C不一定是類A的朋友。
思考: c 是純面向對象的嗎?
如果一個類被聲明為friend,意味着它不是這個類的成員函數,卻可以修改這個類的私有成員,而且必須列在類的定義中,因此他是一個特權函數。c 不是完全的面向對象語言,而隻是一個混合産品。增加friend關鍵字隻是用來解決一些實際問題,這也說明這種語言是不純的。畢竟c 設計的目的是為了實用性,而不是追求理想的抽象。 — Thinking in C
5.2 課堂練習
請編寫電視機類,電視機有開機和關機狀态,有音量,有頻道,提供音量操作的方法,頻道操作的方法。由于電視機隻能逐一調整頻道,不能指定頻道,增加遙控類,遙控類除了擁有電視機已有的功能,再增加根據輸入調台功能。
提示:遙控器可作為電視機類的友元類。
class Remote; class television{ friend class Remote; public: enum{ On,Off }; //電視狀态 enum{ minVol,maxVol = 100 }; //音量從0到100 enum{ minChannel = 1,maxChannel = 255 }; //頻道從1到255 Television(){ mState = Off; mVolume = minVol; mChannel = minChannel; } //打開電視機 void OnOrOff(){ this->mState = (this->mState == On ? Off : On); } //調高音量 void VolumeUp(){ if (this->mVolume >= maxVol){ return; } this->mVolume ; } //調低音量 void VolumeDown(){ if (this->mVolume <= minVol){ return; } this->mVolume--; } //更換電視頻道 void ChannelUp(){ if (this->mChannel >= maxChannel){ return; } this->mChannel ; } void ChannelDown(){ if (this->mChannel <= minChannel){ return; } this->mChannel--; } //展示當前電視狀态信息 void ShowTeleState(){ cout << "開機狀态:" << (mState == On ? "已開機" : "已關機") << endl; if (mState == On){ cout << "當前音量:" << mVolume << endl; cout << "當前頻道:" << mChannel << endl; } cout << "-------------" << endl; } private: int mState; //電視狀态,開機,還是關機 int mVolume; //電視機音量 int mChannel; //電視頻道 }; //電視機調台隻能一個一個的調,遙控可以指定頻道 //電視遙控器 class Remote{ public: Remote(Television* television){ pTelevision = television; } public: void OnOrOff(){ pTelevision->OnOrOff(); } //調高音量 void VolumeUp(){ pTelevision->VolumeUp(); } //調低音量 void VolumeDown(){ pTelevision->VolumeDown(); } //更換電視頻道 void ChannelUp(){ pTelevision->ChannelUp(); } void ChannelDown(){ pTelevision->ChannelDown(); } //設置頻道 遙控新增功能 void SetChannel(int channel){ if (channel < Television::minChannel || channel > Television::maxChannel){ return; } pTelevision->mChannel = channel; } //顯示電視當前信息 void ShowTeleState(){ pTelevision->ShowTeleState(); } private: Television* pTelevision; }; //直接操作電視 void test01(){ Television television; television.ShowTeleState(); television.OnOrOff(); //開機 television.VolumeUp(); //增加音量 1 television.VolumeUp(); //增加音量 1 television.VolumeUp(); //增加音量 1 television.VolumeUp(); //增加音量 1 television.ChannelUp(); //頻道 1 television.ChannelUp(); //頻道 1 television.ShowTeleState(); } //通過遙控操作電視 void test02(){ //創建電視 Television television; //創建遙控 Remote remote(&television); remote.OnOrOff(); remote.ChannelUp();//頻道 1 remote.ChannelUp();//頻道 1 remote.ChannelUp();//頻道 1 remote.VolumeUp();//音量 1 remote.VolumeUp();//音量 1 remote.VolumeUp();//音量 1 remote.VolumeUp();//音量 1 remote.ShowTeleState(); }
5 強化訓練(數組類封裝)
MyArray.h
#ifndef MYARRAY_H #define MYARRAY_H class MyArray{ public: //無參構造函數,用戶沒有指定容量,則初始化為100 MyArray(); //有參構造函數,用戶指定容量初始化 explicit MyArray(int capacity); //用戶操作接口 //根據位置添加元素 void SetData(int pos, int val); //獲得指定位置數據 int GetData(int pos); //尾插法 void PushBack(int val); //獲得長度 int GetLength(); //析構函數,釋放數組空間 ~MyArray(); private: int mCapacity; //數組一共可容納多少個元素 int mSize; //當前有多少個元素 int* pAdress; //指向存儲數據的空間 }; #endif
MyArray.cpp
#include"MyArray.h" MyArray::MyArray(){ this->mCapacity = 100; this->mSize = 0; //在堆開辟空間 this->pAdress = new int[this->mCapacity]; } //有參構造函數,用戶指定容量初始化 MyArray::MyArray(int capacity){ this->mCapacity = capacity; this->mSize = 0; //在堆開辟空間 this->pAdress = new int[capacity]; } //根據位置添加元素 void MyArray::SetData(int pos, int val){ if (pos < 0 || pos > mCapacity - 1){ return; } pAdress[pos] = val; } //獲得指定位置數據 int MyArray::GetData(int pos){ return pAdress[pos]; } //尾插法 void MyArray::PushBack(int val){ if (mSize >= mCapacity){ return; } this->pAdress[mSize] = val; this->mSize ; } //獲得長度 int MyArray::GetLength(){ return this->mSize; } //析構函數,釋放數組空間 MyArray::~MyArray(){ if (this->pAdress != nullptr){ delete[] this->pAdress; } }
TestMyArray.cpp
#include"MyArray.h" void test(){ //創建數組 MyArray myarray(50); //數組中插入元素 for (int i = 0; i < 50; i ){ //尾插法 myarray.PushBack(i); //myarray.SetData(i, i); } //打印數組中元素 for (int i = 0; i < myarray.GetLength(); i ){ cout << myarray.GetData(i) << " "; } cout << endl; }
最後,如果你想學C/C 可以私信小編“01”獲取素材資料以及開發工具和聽課權限哦!
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!