?
開局之前,先來個小插曲,放松一下:
讓我們的熊二來消滅所有bug
?
各位,在這祝我們:
?
?
放松過后,開始步入正軌吧。愛學習的鐵子們:
目錄:
一·類的定義:
1.簡述:
2.訪問限定符:
?3.類域:
二·實例化及類的對象大小:
三·this指針的簡述:
四·類的默認成員函數:
1.構造函數:
2.析構函數:
3·拷貝構造函數:
4.運算符重載:
4.1賦值運算符重載:
4.2運算符重載實現日期類計算器:
4.3取地址運算符重載:
五·額外補充:
5.1初始化列表:
5.2?類型轉換:
5.3 static 成員:
5.4 友元:
5.5內部類:
5.6匿名對象:
5.7對象拷?時的編譯器優化:
一·類的定義:
1.簡述:
樸素點來說類就是在c++中對c中的結構體(struct)的優化,升級成了class。
class為定義類的關鍵字,Stack為類的名字,{}中為類的主體,注意類定義結束時后?分號不能省 略。類體中內容稱為類的成員:類中的變量稱為類的屬性或成員變量;類中的函數稱為類的?法或 者成員函數。
C++中struct也可以定義類,C++兼容C中struct的?法,同時struct升級成了類,明顯的變化是 struct中可以定義函數,?般情況下我們還是推薦?class定義類。
為了區分類中的成員變量和函數傳參的量,通常在成員變量前或后加上_。
對類里面的函數一般默認是inline修飾的內聯函數。
2.訪問限定符:
可以分為 public,private,protected。可以理解為public在類中屬于公有的外界也可以訪問,但是后兩者大致可以認為私有的,只有類體內自己可以訪問到,在外界沒有權限訪問。
使用一般遵循這幾點:
①訪問權限作?域從該訪問限定符出現的位置開始直到下?個訪問限定符出現時為?,如果后?沒有 訪問限定符,作?域就到}即類結束。
②class定義成員沒有被訪問限定符修飾時默認為private,struct默認為public。
③?般成員變量都會被限制為private/protected,需要給別?使?的成員函數會放為public。?
?3.類域:
類定義了?個新的作?域,類的所有成員都在類的作?域中,在類體外定義成員時,需要使?類名+::指明成員屬于哪個類域。
這樣就定義好一個簡單的類了。
??
class date {
public:void init() {_year = 2024;_month =7;_day = 10;}void print() {cout << _year << "/" << _month << "/" << _day << endl;
}
private:int _year;int _month;int _day;};?
二·實例化及類的對象大小:
實例化:類里只是限定了有哪些成員/對象,并沒有真實為它們開辟空間,故還需要外面調用初始化等操作來完成,即真實地創造類的實例化對象。
也可以簡單的想象成類就是造房子的圖紙,并不是真正的房子,還需要給它拿實物搭建,故類的實例化就像拿圖紙造房子一樣。
所謂對象大小可以簡單理解為,把類看成c語言中定義的結構體,即它的每個對象都占有一定的字節大小,而類似結構體對齊的方法可以確定類的大小。
順便說一句,為什么要結構體對齊要存在空出多余的呢?:這是由于cpu讀取內存會從某個固定整數倍讀取,如果空出來的話,可以提高cpu速率,而直接挨著排,可能一次性不能讀完整,故還需要整合,即這里采用的是對齊方法。?
三·this指針的簡述:
this就是在類的對象函數的形參形成的時候會有一個隱式的類的指針變量,類型就是類的名字。
注意:①在類中完成查詢某個類體的成員,賦值等操作都是通過它完成的。
②在函數的實參形參,不能出現this而編譯器默認已經加上了,但是函數內容里面可以用。
③this指針作為函數形參隨函數而動,故存在于內存棧區。
void init() {this->_year = 2024;this->_month =7;this->_day = 10;}
四·類的默認成員函數:
1.構造函數:
可以理解為對類的初始化的函數。
特點:①函數名字與類名相同故寫的時候只用寫一個類名(如:date(){})。
②無返回值無返回類型。
③可以進行重載。
④可以分為全有參,全缺省,無參,半卻省等,當如果調用的是無參函數或者是不想輸入參數的全缺省故無需();如:date d1。
⑤默認構造函數:分為無參構造函數,全缺省構造函數,編譯器默認生成的無參構造函數,三者只能出現一個。
總結:一般構造函數都是自己操作完成,很少編譯器自己完成,比如棧stack就需要由于開辟了空間和賦值,這時就需要自己完成構造函數,而對于兩個棧合成的隊列,這時候在隊列中讓它自己調用棧完成即可,故無需自己構造函數。
class date {
public:void init() {this->_year = 2024;this->_month =7;this->_day = 10;}void print() {cout << this->_year << "/" << this->_month << "/" << this->_day << endl;}//如果外部定義的p為空指針,而這兩個函數執行的時候要有解引用操作,故崩潰//全省的構造:/*date(int year = 2024, int month = 7, int day = 10) {_year = year;_month = month;_day = day;}*///全參構造:/*date(int year, int month, int day) {_year = year;_month = month;_day = day;}*///無參構造:/*date() {_year = 2024;_month = 7;_day = 10;}*///半省函數:date(int year, int month, int day=10) {_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;};int main()
{date d1;//對編譯器自己生成的或者無參的調用d1.init();d1.print();date d1(2024,7);//半缺省的調用d1.print();
return 0;
}
后面在析構函數內結合構造函數對棧,隊列的構建結合。
2.析構函數:
大致就是對已經初始化的對象所帶來的空間資源等進行銷毀操作。
特點:①類似構造函數,但是它都是無參的,調用也同無參的構造函數同,定義是在名前加~(如~date(){})。
②無參數無返回值。
③生命周期快結束時候自動調用。
④編譯器可自己完成析構并自動調用。
⑤一般有資源申請的時候一定要寫析構,否則出現資源泄露,沒有的時候可以不寫,如:stack類一定要寫,而myqueue可以不寫,下面會提到。
從此可以看出c++在初始化和銷毀做到了對c的優化。
~stack() {//程序結束后自動調用free(_a);_a = nullptr;_top = _capacity = 0;if (_a == nullptr) {cout << "_a空" << endl;}}
構造函數和析構函數結合:
class stack {
public:stack(int n=4){_a = (int*)malloc(sizeof(int) * n);_capacity = n;_top = 0;if (_a != nullptr) {cout << "a非空" << endl;}}~stack() {//程序結束后自動調用free(_a);_a = nullptr;_top = _capacity = 0;if (_a == nullptr) {cout << "_a空" << endl;}}private:size_t _capacity;size_t _top;int* _a;
};class MyQueue
{
public://由于是有兩個棧類合成的隊列,故可以省略構造和析構,到最后還是會調用棧類,從這個里面//構造和析構。
private:stack pushst;stack popst;int size;
};int main() {MyQueue mq;return 0;
}
對于這兩個棧類合成的隊列:隊列既沒有初始化即沒寫構造函數,又沒寫析構函數,而都靠的是它的類的成員對象自己調用的自己棧類里面的構造和析構。?
3·拷貝構造函數:
簡單就是一種特殊的構造函數,但是它要求第一個參數必須是自身引用類型,剩下的也可以有參數,但必須有默認值。
為什么要是這樣?
Date(const Date& d){_year = d._year; _month = d._month;_day = d._day;}Date(const Date d){_year = d._year; _month = d._month;_day = d._day;}int main(){Date d1;
Date d2(d1);//如果調用第二個,由于是傳參,首先要調用拷貝構造,這樣會一直循環調用下去,而第一個是引用,直接就是d1故不用這樣。
return 0;
}
①這里如果是調用拷貝構造有兩種寫法:date d2(d1); date d2=d1;? ? 這樣中一樣,故可以選一種。
注:在c++中規定對類 類型的傳值傳參都要先調用它 的拷貝構造函數,而引用無需。
class date {
public:void init() {this->_year = 2024;this->_month = 7;this->_day = 12;}void print() {cout << this->_year << "/" << this->_month << "/" << this->_day << endl;}date(int year, int month, int day) {_year = year;_month = month;_day = day;}
//拷貝構造函數:
date(const date&d) {_year = d._year;_month = d._month;_day = d._day;//這里也可以不寫拷貝構造,無資源的發生,編譯器可以自己調用
}private:int _year;int _month;int _day;};int main() {date d1;//自己調用拷貝構造函數兩種方式://date d2(d1);//date d2 = d1;//d2.print();date d2(2024, 7, 13);d1.func(d2);//d2調用拷貝構造函數:date dc=d2;然后把dc進入func完成操作。d1.print();return 0;
}
②拷貝構造函數它是構造函數的一個重載。
③對于編譯器自己生成的拷貝構造都是淺拷貝(一個一個字節的拷貝)(如有的地方會開辟空間,有資源的時候不適用,因為這樣會使它們的地址相同,然后會析構兩次,程序就會崩潰)因此,需要自己來寫深拷貝來完成如stack類就需要自己開辟空間釋放等。
下面是一段stack和兩個stack合成的Myqueue類代碼:
class stack {
public:stack(int n = 4) { _a = (int*)malloc(sizeof(int) * n);_capacity = n;_top = 0;cout << "Stack()" << endl;}~stack() {//程序結束后自動調用cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}//棧的拷貝構造:假想在要拷貝的類的對象里面完成的把st拷貝給這個對象stack(const stack& st)//有空間的開辟,要自己寫拷貝構造{cout << "stack(const stack& st)" << endl;_a = (int*)malloc(sizeof(int) * st._capacity);if (nullptr == _a){perror("malloc申請空間失敗!!!");return;}memcpy(_a, st._a, sizeof(int) * st._top);_top = st._top;_capacity = st._capacity;}void push(int x)//這里由于參數不是類的類型故調用push函數的時候不需要自己掉拷貝構造{if (_top == _capacity){int newcapacity = _capacity * 2;int* tmp = (int*)realloc(_a, newcapacity *sizeof(int));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}private:size_t _capacity;size_t _top;int* _a;
};class MyQueue
{
public:private:stack pushst;stack popst;int size;
};
當把1推進s1,并且拷貝給s2(此時不注釋掉自己寫的拷貝構造):?
int main() {stack s1;s1.push(1);
stack s2(s1);
return 0;}
?
第一個打印是首先對s1的構造函數進行的初始化;接著把1推進s1,第二個打印是把s1通過拷貝構造給s2,接著后面兩次就是當快結束程序的時候遵循類似棧的后進先銷毀的模式,先對s2,再對s1銷毀?。
當如果把自己寫的拷貝函數注釋掉,再次重復會發生什么呢?
可以看到程序崩潰了。
那為什么呢?
int main() {stack s1;s1.push(1);stack s2(s1); //由于為寫拷貝構造函數,編譯器用自己默認的,即淺拷貝,這時候地址拷貝的s1與s2就是相同的,那么當s2這塊空間被free掉后,s1又會對同一塊被銷毀的區域再次銷毀,自然就崩潰了。return 0; }
對Myqueue無需寫構造,拷貝構造,析構,一切都交給了它的成員變量stack了。
int main() {MyQueue mq;MyQueue mmq(mq);return 0; }
對Myqueue的分析:?
這個類里面都是編譯器默認生成的默認成員函數;故每次都是先對它的成員變量的操作,即進入Myqueue的默認函數然后再次跳轉到對stack的一系列構造;拷貝構造以及析構等,靠stack來對它完成。
?④傳值和傳引用的返回區別:引用返回會直接返回,而傳值返回最后會對結果拷貝一下。就是區分返回類型是否被引用了,被引用的話則要保證對象出了函數作用域不被銷毀。如:
stack &func(){
stack st;
return st;
}
int main(){
stack ret=func();
return 0;
}//如果這樣操作的話直接返回的就是指向函數對st操作的區域,當函數被銷毀后,將會出現野指針隨便指向區域現象
此問題可以函數域內靠static固定在靜態區解決。
對拷貝構造函數的總結:只要有資源產生或者銷毀方面,就要自己寫拷貝構造函數來完成一系列操作。?
4.運算符重載:
①運算符重載是具有特名字的函數,他的名字是由operator和后?要定義的運算符共同構成。和其他函數?樣,它也具有其返回類型和參數列表以及函數體。
②當對類體使用運算符的時候,必須有對應的運算符重載,即對應的函數,否則報錯。
③比如比較大小,這個函數可以寫在類內,也可以是類外:
類外:由于變量是private,則需要從類的public內寫獲取變量值返回函數。
/ bool operator==(date d1,date d2) {
// return d1.getyear() == d2.getyear()
// && d1.getmonth() == d2.getmonth()
// && d1.getday() == d2.getday();
//}
// bool operator>(date d1, date d2) {
// return d1.getyear() == d2.getyear()
// && d1.getmonth() >d2.getmonth()
// && d1.getday() == d2.getday();
// }//可以假設年和日都相同,比較月大小
// bool operator<(date d1, date d2) {
// return d1.getyear() == d2.getyear()
// && d1.getmonth() < d2.getmonth()
// && d1.getday() == d2.getday();
// }
類內:(調用訪問的時候是從一個類體的指針去訪問這個函數,然后進入這個函數,由于傳參是另一個類體,故得到指針,再得到它的對應的變量值與前者的變量值比較最后返回。因此如果是類內定義可以減少參數的量)
class date {
public:void init() {this->_year = 2024;this->_month =7;this->_day = 10;}void print() {cout << this->_year << "/" << this->_month << "/" << this->_day << endl;}//由于私有外界無法訪問故可以把它的值當做返回值于公有返回。int getyear() {return _year;}int getmonth() {return _month;}int getday() {return _day;}bool operator==(date d2) {return _year == d2._year &&_month == d2._month &&_day == d2._day;}private:int _year;int _month;int _day;};
主函數:
date d1(2024,7);date d2(2024,8);date d3(2024, 6);/*cout << operator==(d1, d2) << endl;cout << (d1==d2) << endl;//d1==d2會自動轉換為上面的operator==(d1, d2) cout << operator>(d1, d2) << endl;cout << (d1 > d2) << endl;cout << operator<(d1, d2) << endl;cout << (d1 < d2) << endl;*/cout << d1.operator==(d2) << endl;cout << (d1==d2)<< endl;//指向d1的指針去訪問:date* p = &d1;cout << p->operator==(d2) << endl;
④運算符重載后,它的優先級與結合性應與內置類型保持相同。?
即當調用的是運算重載,但內外運算順序不變。
⑤不能通過連接語法中沒有的符號來創建新的操作符:?如operator@。
⑥.*| ::| sizeof| ?:| . 注意以上5個運算符不能重載。
⑦如重載有前置++和后置++,應該有區分,后置++要求有一個int類型參數,又由于函數本身沒用到它,故可以不用接收寫成(int),而前置沒有。
4.1賦值運算符重載:
屬于一種深拷貝或者淺拷貝的一種類型;即有兩個初始化完成的類對象,之間的賦值過程;
也許他會和拷貝構造聯系但是拷貝構造是把一個對象的值給一個未出現的對象的初始化。
它的一些規定及特點:
①一般用const修飾,而且定義必須當成員函數。?
②一般參數類型也要用引用,如果有返回值(比如連續賦值操作)返回類型也要用引用,提高了效率。
③未寫此處,編譯器也會默認生成,不過生成的就是淺拷貝了(參數無引用的話)。
④類似date類的,編譯器也會自己實現賦值操作,可以不用寫,而類似stack類的,存在資源空間就要自己寫。
代碼寫法:
對自寫的賦值運算符函數實現:
date &operator=(const date& d) {_year = d._year;_month = d._month;_day = d._day;return *this;
}//由于想要進行連續賦值操作,故這里返回類型引用一下并且讓它能返回被賦值后的對象。
完整的class date:?
class date {
public:void init() {this->_year = 2024;this->_month = 7;this->_day = 12;}void print() {cout << this->_year << "/" << this->_month << "/" << this->_day << endl;}date(int year, int month, int day) {_year = year;_month = month;_day = day;}//拷貝構造函數:date(const date&d) {_year = d._year;_month = d._month;_day = d._day;}//被傳參的函數:void func(date d) {d.print();cout << &d << endl;}date &operator=(const date& d) {_year = d._year;_month = d._month;_day = d._day;return *this;}
private:int _year;int _month;int _day;};
int main() {//date d1(2024, 7, 12);//date d2(2024, 7, 13);//date d3 (2024, 7, 11);//d3=d2 = d1;//編譯器默認的淺拷貝,可以不用寫//d2.print();//d3.print();date d1(2024, 7, 12);date d2(2024, 7, 13);date d3 (2024, 7, 11);//自行實現的賦值重載;可連賦d3.operator=(d2);d3.print();d3 = d2 = d1;d3.operator=(d2.operator=(d1));//兩種寫法等同return 0;
}
最后總結:只要顯示了構造,析構等存在資源開辟及銷毀;就要寫拷貝構造和賦值重載。
4.2運算符重載實現日期類計算器:
下面是用運算符重載完成的一系列函數實現的日期類:
date.h:
#pragma once
#pragma once
#include<iostream>
using namespace std;
#include<assert.h>class Date
{
public://防止當這兩個函數在類里面的話會出現d1<<cout等情況,這里放在外面,可是不能用類里的成員變量//故用友好函數,告訴類,讓它在外面可以用到friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d) {_year = d._year;_month = d._month;_day = d._day;}~Date() {_year = 0;_month =0 ;_day = 0;}bool CheckDate() {if (_month < 1 || _month > 12|| _day < 1 || _day > getday(_year, _month) || _year < 1){return false;}else{return true;}}void Print();int getday(int year, int month) {assert(month > 0 && month < 13);static int arr[13] = { 0,31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31 };if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){return 29;}return arr[month];}bool operator<(const Date& d) const;bool operator<=(const Date& d) const;bool operator>(const Date& d) const;bool operator>=(const Date& d)const;bool operator==(const Date& d) const;bool operator!=(const Date& d)const;Date operator+(int day)const;Date& operator+=(int day);Date& operator-=(int day);Date operator-(int day)const;//前后置++--:后置一般多一個int參數,而這個參數對函數沒有故不用接收Date operator++(int);Date& operator++();Date operator--(int);Date& operator--();//類對象之間的相減int operator-(const Date& d);//取地址重載函數/*Date* operator&(){return (Date*)0xf2134214;}*//*const Date* operator&(int){return (Date*)0xf2134214;}*/private:int _year;int _month;int _day;
};//為了可以類似cout<<a<<b<<endl;:這里讓它有返回值,防止返回拷貝故用了返回引用
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
date.cpp:
#include"date.h"void Date::Print()
{cout << _year << "/" << _month << "/" << _day << endl;
}//比如d1+=1003;
//這里要在+里面引用+=:/運算比較::Date& Date:: operator+=(int day) {if (day < 0) {return *this -= (-day);//判斷輸入的day是負數情況}_day += day;while (_day > getday(_year, _month)) {_day -= getday(_year, _month);_month++;if (_month > 12) {_year++;_month = 1;}}return *this;
}Date Date:: operator+(int day)const {Date tmp = *this;//對tmp操作:tmp.operator+=(day);return tmp;
}
//在-里引用-=://Date Date::operator-(int day)
//{
// Date tmp = *this;
// tmp._day -= day;
// while (tmp._day <= 0)
// {
// --tmp._month;
// if (tmp._month == 0)
// {
// tmp._month = 12;
// --tmp._year;
// }
//
// tmp._day += GetMonthDay(tmp._year, tmp._month);
// }
//
// return tmp;
//}
////Date& Date::operator-=(int day)
//{
// *this = *this - day;
//
// return *this;
//}
// 上面要拷貝三次不推薦
//下面的方式只要拷貝2次:
Date& Date::operator-=(int day) {if (day < 0) {return *this += (-day);//判斷輸入的day是負數情況}_day -= day;while (_day <= 0) {if (_month > 0)_month--;else {_year--;_month = 12;}_day += getday(_year, _month);}return *this;
}//保證要輸入的對象不變
Date Date::operator-(int day)const {Date tmp = *this;tmp -= day;return tmp;
}bool Date::operator==(const Date& d)const
{return _year == d._year&& _month == d._month&& _day == d._day;
}
bool Date::operator<(const Date& d)const {if (_year < d._year) {return true;}else if (_year == d._year) {if (_month < d._month) {return true;}else if (_month == d._month) {if (_day < d._day) {return true;}else {return false;}}else {return false;}}else {return false;}
}bool Date:: operator>(const Date& d)const {return !(*this <= d);
}bool Date::operator<=(const Date& d) const
{return (*this < d) || (*this == d);
}bool Date::operator>=(const Date& d) const {return (*this > d) || (*this == d);
}
bool Date::operator!=(const Date& d)const {return!(*this == d);
}//前后置++--:Date Date::operator++(int)
{Date tmp = *this;*this += 1;return tmp;//由于在函數里面有定義的tmp,出了函數銷毀,故需要臨走的時候需要拷貝故不用返回引用
}Date& Date::operator++()
{*this += 1;//這里里面沒有定義,而是對this外部傳進的直接操作,為了提高效率可以返回的時候引用return *this;
}Date Date::operator--(int)
{Date tmp = *this;*this -= 1;return tmp;
}Date& Date::operator--()
{*this -= 1;return *this;
}<< >>的重載:ostream& operator<<(ostream& out, const Date& d) {out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;}
istream& operator>>(istream& in, Date& d) {int jud = 1;while (jud) {cout << "請輸入日期<";in >> d._year >> d._month >> d._day;if (!d.CheckDate()){cout << "輸入日期非法:";d.Print();cout << "請重新輸入!!!" << endl;}jud = 0;}return in;
}int Date::operator-(const Date& d) {//先用假設法假設好max與min,然后判斷,不符合的話就再換Date max = *this;Date min = d;int n = 0;int flag = 1;if (min > *this) {min = *this;max = d;flag = -1;}while (min != max) {min++;n++;}return flag * n;}
通過它可以簡單實現一些對日期的計算,類似于日期計算器:
網址鏈接:日期計算器
4.3取地址運算符重載:
首先先認識一下const成員函數:類內不希望被修改的成員函數可以在函數后加上const。
如:對于成員函數:
void print()const{...
}//這個參數就是this指針 類型:date const*this;而編譯器已經不希望被修改this;但是可以修改它指向的對象,如果用const修飾這個成員函數,相當于對this指針操作:使它變成 const date const *this;這樣的話,它指向的也不能改了。
//調用這個被const修飾的成員函數前先對外面的對象初始化:
const date d1//相當于權限平移date d1//相當于權限縮小
后面就是用到取地址運算符重載 :?
即通過得到對象里面的地址;返回是類的類型;如果直接調用如&d1;編譯器會自己生成地址運算符重載函數,可直接用;也可以自己在類里面寫,但是注意:當寫的比如改動地址想讓它調用這個,必須要和編譯器默認的函數互為重載才能調用。
寫法:
class date{public:
date*operator&(){
return ...}private:
...
};
對于const對象取地址也就是限制了返回類型,是無法修改的,其他和普通對象取法一樣。
默認成員函數寫法總結:都是可以寫在類里面的,而比如構造析構拷貝構造,一定要在類里面(如果分文件,如.h;.cpp;那就在.h里聲明,.cpp里定義且要明確一下如:date::這樣也相當于在類里)?;運算符重載函數除了賦值運算符要寫在類里;其他都可,但是寫在類里可以減少一個參數(this所指)。
五·額外補充:
5.1初始化列表:
①首先它是構造函數的一種方式;初始化列表的使??式是以?個冒號開始,接著是?個以逗號分隔的數據成 員列表,每個"成員變量"后?跟?個放在括號中的初始值或表達式。
如:
class A{public:A(int a):_a1(a),a2(_a1){}void print(){}private:int _a1=1;int _a2=1;
}
?②初始化列表只能出現一次,因為這是成員定義的地方。
③對于聲明的時候給的值是缺省值,也就是如果初始化列表沒有這個成員變量,然后他會用缺省值。
④對有const類變量必須在初始化列表定義或者用缺省值,因為它無默認構造;對于引用也是必須有初始化即在初始化列表中定義。
⑤編譯器是按照聲明順序去給變量定義的,故盡量把聲明和定義順序寫一致。
初始化列表的步驟過程總結:編譯器從private這里的聲明處開始一步步往下走,<1>有對應的初始化列表就調用,<2>沒有的話看看有無缺省值,a有就用,如果在沒有,b就要靠默認構造了,b<1>對于內置類型,編譯器生成的默認構造是隨機值;b<2>而自定義類的類型如果自己沒寫默認構造將會報錯。
下面是有關初始化列表調用的題:
#include<iostream>
using namespace std;
class A
{
public:A(int a):_a1(a), _a2(_a1){}void Print() {cout << _a1 << " " << _a2 << endl;}
private:int _a2 = 2;int _a1 = 2;
};
int main()
{A aa(1);aa.Print();
}
來看看這道題最后輸出什么?
首先聲明先遇到的是_a2,到對應的列表去找結果它的值是_a1,而_a1此時還未初始化,故是隨機值;然后聲明就走到了_a1,對應main函數中的1;故最后結果是 1和隨機值。?
5.2?類型轉換:
即c++支持類內隱式類型轉化為類的類型對象,然而類內應該有對應的輸入類型的成員函數。
舉例:
class A {...A(int a) {...}A(int a1, int a2) {...}void print() {...}...
};
int main() {A aa1 = 1;//簡單理解為:1定義的一個類類型的對象拷貝給aa1。//具體過程:首先拿1構建一個類類型的臨時對象然后再拷貝構造給aa1// (對于連續的構造及拷構,編譯器優化成直接構造)aa1.print();const A& aa2 = 1;//由于出現了臨時對象,表現常性,故用const接收確保它不能修改//c++11后優化了對兩個參數的類型轉化即:A aa3 = { 1,2 };return 0;
}
這樣的話是不是看不到它的用途呢?下面看一下它的應用,可以簡化代碼步驟:
class A {...A(int a) {...}A(int a1, int a2) {...}void print() {...}...
};
class stack {
public:void push(const A &aa) {...}private:A_arr[10];int_top;
};
int main() {//第一種寫法:stack st;st.push({ 2,2 });//第二種寫法:stack st;A aa3(2,2);st.push(aa3);//明顯利用了類型轉換減少步驟。return 0;
}
故對類似這種類的嵌套等就可以用這樣的類型轉換減少步驟。?
5.3 static 成員:
①靜態變量屬于類,不屬于某個對象,而位于靜態區,隸屬全局;即類內聲明,類外定義。
②如:類內:private: static int _a; 類外:int 類名:: _a=1;(這里外部訪問靜態量或函數必須打破類域即用類名:: )。
③用static 修飾的成員函數即靜態函數,類似全局的,故無this指針,所以不能訪問類內的其他成員,只能訪問靜態的而非靜態函數既可訪問靜態也可以訪問非靜態。
④靜態成員也是類的成員也受訪問限定符的限定即如果放在private也是外部無法訪問。
⑤靜態成員的訪問可以用類名+::或者對象+.來獲取。
總結:static既有部分類的性質(如:受訪問限定符限定,外部在private無法訪問)又有全局的性質(如:定義要在類外面,無this指針,訪問成員有權限等)。
例題:
求1+2+3+...+n_牛客題霸_牛客網
這里就是應用了static成員:
//思路:由于限制了一些判斷;故用累加,可以多次調用類對象的初始化,而防止初始化每次改變成員變量
//的初始值,把成員變量static成在類中類似的全局變量,對它累加即可。
//class sum {
//public:
// sum() {
// _ret += _i;
// _i++;
// }
// static int re_ret() {
// return _ret;
// }
//private:
// static int _i;
// static int _ret;//當多次用構造函數初始化,而不希望改變一開始的值可以考慮用static
//};
//
//int sum::_i = 1;
//int sum::_ret = 0;//在類內聲明,在類外定義,static專屬的外部用要突破類域用類名+::
//
//class Solution {
//public:
// int Sum_Solution(int n) {
// // sum a[n];vs不支持變長數組:這里初始化了一個sum類型變長數組
// sum* p = new sum [n];//用new來開辟空間sum類型,類似malloc 后面是類型+數組大小
// delete[]p;
// return sum::re_ret();//static函數外部用亦是如此。
// }
//};
5.4 友元:
①友元分為友元函數和友元類:
②友元函數:從外部定義函數,類里面用friend+聲明;就可以讓這個函數通過比如對象+.形式去訪問類成員。(可以在任意地方聲明,不受訪問限定符限定)
③一個函數可以是多個類的友元。(即可以訪問多個類的成員)
④對于友元類:如果b是a的友元類,那么b就可以訪問a的成員。(多側重于a的私有的時候常用,即在b類里如函數存在a類的對象可以以此訪問a的私有)如:
class A
{// 友元聲明friend class B;
private:int _a1 = 1;int _a2 = 2;
};
class B
{
public:void func1(const A& aa){cout << aa._a1 << endl;cout << _b1 << endl;}void func2(const A& aa){cout << aa._a2 << endl;cout << _b2 << endl;}
private:int _b1 = 3;int _b2 = 4;
};
int main()
{A aa;B bb;bb.func1(aa);bb.func1(aa);return 0;
}
⑤友元類的關系是單向的,不具有交換性,?如A類是B類的友元,但是B類不是A類的友元;?友元類關系不能傳遞,如果A是B的友元,B是C的友元,但是A不是B的友元。
⑥友元特征:有時提供了便利。但是友元會增加耦合度,破壞了封裝,所以友元不宜多?。
5.5內部類:
①即兩個類的嵌套;即?個類定義在另?個類的內部;內部類是?個獨?的類,跟定義在 全局相?,他只是受外部類類域限制和訪問限定符限制,所以外部類定義的對象中不包含內部類。
②內部類默認是外部類的友元類。
③如果一個a類設計出來就是為了只給b類用,那么可以考慮把a放入b的private;成為b的專屬類,則只有b可以用,外部無法用。比如下面的例題。?
總結:內部類相比于分開定義兩個類區別:1.內部類會自動變成外部類的友好類;2.限定符限定和類域限定:a:即如果把內部類放到public則需要外部用外部類+::突破外部類域才能訪問到,而對應外部類訪問它就像類外訪問一個類一樣。b:而放到private或protected就只能外部類訪問,類外無法訪問。
對于上面那道static的題也可以用上內部類;由于設計出的sum類如果只是想給Solution用,其他外部無法調用可以把它放入私有,如:
//內部類應用:比如只讓 Solution能用sum,放在它的私有
class Solution {
public:int Sum_Solution(int n) {// sum a[n];sum* p = new sum[n];return sum::re_ret();}private:static int _i;static int _ret;//讓它少突破一層類域的限制class sum {public:sum() {_ret += _i;_i++;}static int re_ret() {return _ret;}};};
int Solution::_ret = 0;
int Solution::_i = 1;
?
5.6匿名對象:
①即用類型定義出的沒有名字的對象。
如:
a();
a(1);
//以上都是匿名對象
//以下是有名對象
a a1;a a1(1);
//而不能這么定義:a a1()編譯器無法識別是定義還是聲明
②區別:匿名對象作用域只在當前這一行出了后立刻調用析構函數,而有名對象是當前整個域,出了后才析構,相比而言前者范圍更小,故當臨時定義對象的時候可以用匿名,而const可以延長它的生命周期。?
?
5.7對象拷?時的編譯器優化:
①現代編譯器會為了盡可能提?程序的效率,在不影響正確性的情況下會盡可能減少?些傳參和傳參 過程中可以省略的拷?。
?②如何優化C++標準并沒有嚴格規定,各個編譯器會根據情況??處理。當前主流的相對新?點的編 譯器對于連續?個表達式步驟中的連續拷?會進?合并優化,有些更新更"激進"的編譯還會進?跨 ?跨表達式的合并優化。
下面舉個簡單的例子:
class A
{
public:A(int a1 = 0, int a2 = 0):_a1(a1),_a2(a2){...}A(const A& aa):_a1(aa._a1){...}
private:int _a1 = 1;int _a2 = 1;
};
int main(){A a1=1;cosnt A&a2=1;return 0;
}
解釋:數字1是類型轉換,本來是掉A默認構造函數,然后創建臨時對象再默認拷貝給a1;這里優化直接省去了拷貝,直接臨時對象給a1,a2也同理,直接讓調用完的默認構造,直接拿a2引用這個對象。
這樣類似甚至更多的編譯器優化還有很多,比如還有直接把表達式轉換給優化掉的,甚至更加離譜的,都取決于強大編譯器的機制,感興趣的可以去觀察嘗試一下。
以上就是個人對c++類和對象這一方面簡單及其簡化版的理解,外加補充一些生動簡化的例子,希望有助于讀者學習和理解。?
?
?