類中的6個默認成員函數
1.構造函數
什麼是構造函數?
構造函數是一個特殊的函數,名字與類名相同,創建類類型的對象的時候由編譯器自動調用,保證每個數據成員都有一個合適的初始值,並且在對象的生命周期內只能調用一次。
構造函數的特性
1.函數名與類名相同
2.沒有返回值(就是在類名的前面不加任何的類型,void都不可以)
3.構造函數可以重載
4.對象創建的時候會自動調用的對應的構造函數
class Date
{
public:
//下面的兩個構造函數構成重載
Date(int year)//這有一個形參的構造函數(類名的前面不加任何的類型)
{
_year = year;
}
Date(int year, int mouth, int day)//有三個形參的構造函數
{
_year = year;
_mouth = mouth;
_day = day;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1(2018);//調用第一個構造函數
Date d2(2011, 6, 6);//調用第二個構造函數
return 0;
}
5.構造函數可以在類內定義也可以在類外定義
class Date
{
public:
Date(int year);//在類內進行聲明
private:
int _year;
int _mouth;
int _day;
};
Date::Date(int year)//在類外進行實現
{
_year = year;
}
int main()
{
Date d1(2018);//調用第一個構造函數
return 0;
}
6.如果類中沒有顯示定義一個構造函數的話,則c++編譯器會自動的生成一個無參的默認構造函數,一旦用戶在類中定義了一個構造函數的話,編譯器將不會自動生成一個構造函數。
7.無參的構造函數和全預設的構造函數被統稱之為預設構造函數,並且預設構造函數只能有一個。
class Date
{
public:
Date()//沒有參數的構造函數
{
;
}
Date(int year = 2018, int mouth = 6, int day = 10)//全預設參數的構造函數
{
_year = year;
_mouth = mouth;
_day = day;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1;//如果你不穿任何參數,這樣調用的時候就會出錯,因為編譯器不知道調用那個構造函數
return 0;
}
2、析構函數
什麼是析構函數?
析構函數與構造函數的功能相反,在對象被銷毀之前,由編譯器自動的調用,用來做一些類的資源清理和汕尾的工作。
注意: 析構函數體內不是刪除對象,而是僅僅做一些對象刪除前的相關清理工作。
析構函數的特性:
1.析構函數沒有參數沒有返回值。(因為析構函數是在對象刪除時調用的,這時候傳參數是沒有意義的,相當於一個人吃飽飯之後,析構函數相當於幫他收拾餐具而不是再給他食物,因為他已經飽了,所以給他食物是沒有意義的)
2.一個類中析構函數只能有一個,如果用於沒有自己定義的話,編譯器就會自動生成一個析構函數。反之編譯器不會自己生成一個析構函數而是調用用於自己寫的析構函數。
3.析構函數是在類名的前面加一個~
4.對象在生命周期結束的時候編譯器會自動調用析構函數,而不需要用戶自己調用。
class student
{
public:
student()//構造函數
{
_name = (char *)malloc(12);
_sex = (char *)malloc(4);
if(_name == NULL || _sex == NULL)
{
assert(0);
}
_age = 0;
}
~student()//析構函數在類名的前面加~。析構函數是沒有參數和返回值的
{
if(_name)
{
free(_name);
_name = nullptr;
}
if(_sex)
{
free(_sex);
_sex = nullptr;
}
_age = 0;
}
private:
char *_name;
char *_sex;
int _age;
};
int main()
{
student s1;
return 0;
}
3、拷貝構造函數
什麼是拷貝構造函數?
拷貝構造函數只有單個形參,而且這個參數必須是類類型的引用,再用已存在的類類型的對象創建新對象自時動調用。
class Date
{
public:
Date(int year = 2018, int mouth = 6, int day = 10)
{
_year = year;
_mouth = mouth;
_day = day;
}
Date(const Date& d)//只有一個形參,而且是類類型的引用,在前面一般(不是一定)加一個const,因為我們現在不需要改變d1中的內容
{
_year = d._year;
_mouth = d._mouth;
_day = d._day;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1(2000, 1, 1);
Date d2(d1);
return 0;
}
為什麼拷貝構造函數要傳一個引用而不是一個對象?
因為如果傳值的話,會導致無窮遞歸,具體的如下圖所示:
而如果傳的時引用的話,相當於傳的時對象的地址,這樣就不會創建一個臨時的對象。也就不會引發無窮遞歸。
拷貝構造函數的特徵
1.拷貝構造函數是構造函數的一種重載形式。
2.拷貝構造函數的參數只有一個且必須是引用傳參,傳值的方式會引發無限的遞歸調用
3.如果沒有顯示的給出定義,那麼系統會默認的生成一個拷貝構造函數。默認的拷貝構造函數會按照成員的聲明順序依此拷貝類的成員進行初始化。
那些類的拷貝構造函數一定要用戶提供?
如果對象中有資源的話(比如動態開闢一塊空間)一定要給出拷貝構造函數。
4、賦值運算符的重載
如果你想比較兩個類類型的對象的大小,應該怎麼辦?
一般的時候想到的就是定義一個函數來比較,但是這樣的方式不是很直觀。用戶不一定一眼就可以看出你的用途。
這時我們可以用運算符的重載來實現。代碼如下:(實現一個==運算符的重載)
class Date
{
public:
Date(int year = 2018, int mouth = 6, int day = 10)
{
_year = year;
_mouth = mouth;
_day = day;
}
bool IsSameDate(const Date & d)//一般情況下用函數來比較
{
return _year == d._year && _mouth == d._mouth && _day == d._day;
}
bool operator==(const Date & d)//運算符的重載
{
return _year == d._year && _mouth == d._mouth && _day == d._day;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1(2000, 1, 1);
Date d2(d1);
//if(d1.IsSameDate(d2)) //這樣比較的話,可讀性不高,用戶不能一眼看出來它的意思
if(d1 == d2)//這樣比較非常直觀,很容易就直到它的意思
{
cout<<"這是日期相同的一天"<<endl;
}
else
{
cout<<"這兩個日期不相同"<<endl;
}
return 0;
}
什麼是運算符的重載?
運算符是具有特殊函數名的函數,也具有其返回值的類型,函數的名字、參數列表、返回值的類型與普通的函數類似,函數的名字是:operator後面加需要重載的運算符符號。
運算符的重載可以重載成全局函數(當成員變數為公有的話)也可以重載成類的成員函數
注意:
1.不能通過連接其他的符號來創建新的運算符:比如:operator@;
2.重載運算符必須有一個是類類型或者是枚舉類型的操作數。比如不能重載兩個整形的加法運算符。
3.用於內置類型的操作符,其含義不能改變。
4.作為類的成員函數的重載函數,其形參看起來比操作數的數目少一位成員函數的,操作符有一個默認的形參this,限定為第一個形參。
賦值運算的重載
賦值運算符的重載
1.參數類型
2.返回值
3.檢測是否自己給自己賦值
4.返回this
class Date
{
public:
Date(int year = 2000, int mouth = 10, int day = 1)
{
_year = year;
_mouth = mouth;
_day = day;
}
Date & operator=(Date & d)//可以返回引用的時候就盡量返回引用
{
if(this != &d)//避免自己給自己賦值
{
_year = d._year;
_mouth = d._mouth;
_day = d._day;
}
return *this;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1(2018,2,2);
Date d2;
d2 = d1;
return 0;
}
對於上面的運算符的重載函數來說,為什麼要有返回值,為什麼返回值是*this呢?
其實如果是兩個元素之間的賦值的話,不用返回也可以,但是如果是幾個元素的連續賦值的話,就不行了.
比如 a = b = c
對於上面這個連續賦值的操作來說,可以拆分為兩個部分:b = c, a = b.首先是將c的值賦給b,如果沒有返回值的話,後面的賦值運算就會出錯,當然也不可以返回形參d,因為形參d相當於c,如果將c返回去的話,後面的賦值運算就不是a = b 而是a = c,這樣雖然數值上使對的,但是邏輯上是錯的。,所以只能返回*this;
現在我們再來看幾種運算符的重載(++a,a++)
++a和a++的運算符的重載為:
class Data
{
public:
Data(int data = 0)
{
_a = data;
}
Data & operator++()//這是++a的操作
{
_a += 1;
return *this;
}
Data operator++(int)//這是a++的操作
{
Data tmp(*this);
_a += 1;
return tmp;
}
private:
int _a;
};
int main()
{
Data d(10);
++d;
d++;
return 0;
}
有上面的操作可知,++a和其他的操作符的的運算符的重載沒有區別,但是a++有一定的不一樣,如果a++按照前面的方式的話,會和++a的運算符函數重載的參數列表一樣,所以為了區別,給了a++函數了加了一個形參,讓兩個函數形成重載。
下面的一段代碼有問題嗎?
typedef int DataType;
class SeqList
{
public:
SeqList(int capacity = 10)
{
Sq = (DataType *)malloc(sizeof(DataType)*capacity);
if(Sq == NULL)
{
assert(0);
}
_capacity = capacity;
_size = 0;
}
~SeqList()
{
free(Sq);
Sq = NULL;
_capacity = 0;
_size = 0;
}
private:
DataType *Sq;
int _capacity;
int _size;
};
int main()
{
SeqList s1(20);
SeqList s2(s1);
SeqList s3;
s3 = s1;
return 0;
}
這段代碼是有問題的,首先如果一個類中涉及到資源(比如:動態內存開闢空間),一定要自己給出析構函數、拷貝構造函數、賦值運算符函數。 上面的代碼沒有自己給出的拷貝構造函數和賦值運算函數。所以會導致兩個問題:
1.因為上面的代碼中沒有拷貝構造函數,所以s2調用系統默認生成的拷貝構造函數時,s2和s1共用了一塊空間,這也導致了,當s1和s2在對象銷毀時,調用析構函數釋放空間時,一塊空間釋放了兩次,所以會出錯。
2.還有一個問題就是,當s3調用系統默認的賦值運算函數時,s3就會和s1指向同一塊空間,而s3原來的這塊空間就會丟失,導致s3原來的空間沒有釋放而造成內存泄漏。
剛開始創建s3的時候:
當執行s3 = s1這條語句的時候:
此時的s3指向的是s1的空間,而s3原來的空間就丟了。
5.const成員
首先我們來了解一下const,在c++中如果一個const修飾一個變數的話,這個變數就成為了一個常量,具有宏的屬性,在編譯期間會將const所修飾的常量進行替換。
const修飾類的成員函數:
const修飾的成員函數是如何寫的呢?
class Data
{
public:
Data(int data = 0)
{
_a = data;
}
void SetData(int a)const//const修飾成員函數
{
;
}
private:
int _a;
};
是不是覺得這個const位置有點怪呢?是不是覺得(const void SetData(int a))這樣才是正確的呢?
其實不能將const放在類型的前面,因為放在前面修飾的不是成員函數,而是這個函數的返回值。
但是,你會發現用const修飾函數中不能修改類中普通的「成員變數」,因為如下:
此時const修飾的是*this,所以this指針指向的內容不能改變。
如果的const修飾的成員函一定要修改某個成員變數的話,你可以在這個變數的前面加multable這個關鍵字。代碼如下:
class Data
{
public:
Data(int data = 0)
{
_a = data;
}
void SetData(int a)const//const修飾成員函數
{
_a = a;
}
private:
mutable int _a;//在前面加mutable
};
int main()
{
Data d(10);
d.SetData(2);
return 0;
}
下面是關於const修飾成員變數的幾個問題:
1.const修飾的對象可以調用非const修飾的成員函數和const修飾的成員函數嗎?
const修飾的對象可以調用const修飾的成員函數。
cons修飾的對象是不可以修改對象的內容,所以如果調用非const修飾的成員函數時,const成員函數可能會修改對象的內容,這樣對對象來說是不安全的。
2.非const修飾對象可以調用非const修飾的成員函數和const修飾的成員函數嗎?
普通類型的對象可以調用非const修飾的成員函數,並且對函數的內容可讀可修改。
非const修飾的對象為什麼可以調用const修飾的成員函數?因為非const修飾的對象如果不對const修飾的成修改的話,是可以的調用的。所以對const修飾的成員函數只能讀,而不能修改。
3.const修飾的成員函數可以調用其他非const修飾的成員函數和const修飾的成員函數嗎?
const修飾的成員函數可以調用其他的const修飾的成員函數。
const修飾的成員函數不能調用其他的非const修飾的成員函數。因為this指針的類型不同,這個函數的this指針的類型是const test *const this ,而非const修飾的成員函數的this指針為test *const this ,類型不同所以不能調用。
4.非const修飾的成員函數可以調用其他非const修飾的成員函數和const修飾的成員函數嗎?
非const修飾的成員函數可以調用其他的非const修飾的成員函數。
非const修飾的成員函數可以調用其他的const修飾的成員函數,因為當前函數是可讀可寫的,調用const修飾的成員函數是只能讀的,所以如果對const函數進行只讀的操作是可以的。(其實相當於大官(非const修飾的成員函數)可以指揮小官(const修飾的成員函數),也可以讓同級別的官(非const修飾的成員函數)幫忙)。
6.取地址及const取地址操作符的重載
這兩個,默認的成員函數一般不用重新定義,編譯器會重新生成。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1;
const Date d2;
cout<< &d1 <<endl;
cout<< &d2 <<endl;
return 0;
}
上面的兩個函數可以構成重載嗎?
可以,因為他們的this指針的類型不同。
這兩個運算符一般不需要重載,使用編譯器生成默認的取地址的重載即可,只有特殊的情況下才需要重載,比如想讓別人獲取到指定的內容


TAG:程序員小新人學習 |