C++建構函式詳解及顯式呼叫建構函式(explicit)
一. 什麼是拷貝建構函式
首先對於普通型別的物件來說,它們之間的複製是很簡單的,例如:
int a = 100;
int b = a;
而類物件與普通物件不同,類物件內部結構一般較為複雜,存在各種成員變數。
下面看一個類物件拷貝的簡單例子。
#include <iostream> using namespace std; class CExample { private: int a; public: //建構函式 CExample(int b) { a = b;} //一般函式 void Show () { cout<<a<<endl; } }; int main() { CExample A(100); CExample B = A; //注意這裡的物件初始化要呼叫拷貝建構函式,而非賦值 B.Show (); return 0; }
執行程式,螢幕輸出100。從以上程式碼的執行結果可以看出,系統為物件 B 分配了記憶體並完成了與物件 A 的複製過程。就類物件而言,相同型別的類物件是通過拷貝建構函式來完成整個複製過程的。
下面舉例說明拷貝建構函式的工作過程。
#include <iostream> using namespace std; class CExample { private: int a; public: //建構函式 CExample(int b) { a = b;} //拷貝建構函式 CExample(const CExample& C) { a = C.a; } //一般函式 void Show () { cout<<a<<endl; } }; int main() { CExample A(100); CExample B = A; // CExample B(A); 也是一樣的 B.Show (); return 0; }
CExample(const CExample& C) 就是我們自定義的拷貝建構函式。可見,拷貝建構函式是一種特殊的建構函式,函式的名稱必須和類名稱一致,它必須的一個引數是本型別的一個引用變數。
二. 拷貝建構函式的呼叫時機
在C++中,下面三種物件需要呼叫拷貝建構函式!
1. 物件以值傳遞的方式傳入函式引數
class CExample { private: int a; public: //建構函式 CExample(int b) { a = b; cout<<"creat: "<<a<<endl; } //拷貝構造 CExample(const CExample& C) { a = C.a; cout<<"copy"<<endl; } //解構函式 ~CExample() { cout<< "delete: "<<a<<endl; } void Show () { cout<<a<<endl; } }; //全域性函式,傳入的是物件 void g_Fun(CExample C) { cout<<"test"<<endl; } int main() { CExample test(1); //傳入物件 g_Fun(test); return 0; }
呼叫g_Fun()時,會產生以下幾個重要步驟:
(1).test物件傳入形參時,會先會產生一個臨時變數,就叫 C 吧。
(2).然後呼叫拷貝建構函式把test的值給C。 整個這兩個步驟有點像:CExample C(test);
(3).等g_Fun()執行完後, 析構掉 C 物件。
2. 物件以值傳遞的方式從函式返回
class CExample
{
private:
int a;
public:
//建構函式
CExample(int b)
{
a = b;
}
//拷貝構造
CExample(const CExample& C)
{
a = C.a;
cout<<"copy"<<endl;
}
void Show ()
{
cout<<a<<endl;
}
};
//全域性函式
CExample g_Fun()
{
CExample temp(0);
return temp;
}
int main()
{
g_Fun();
return 0;
}
當g_Fun()函式執行到return時,會產生以下幾個重要步驟:
(1). 先會產生一個臨時變數,就叫XXXX吧。
(2). 然後呼叫拷貝建構函式把temp的值給XXXX。整個這兩個步驟有點像:CExample XXXX(temp);
(3). 在函式執行到最後先析構temp區域性變數。
(4). 等g_Fun()執行完後再析構掉XXXX物件。
3. 物件需要通過另外一個物件進行初始化;
CExample A(100);
CExample B = A;
// CExample B(A);
後兩句都會呼叫拷貝建構函式。
以上原文地址:https://blog.csdn.net/lwbeyond/article/details/6202256
補充:
#include<iostream>
using namespace std;
class A
{
public:
A(const A& )
{
cout << "copy"<<endl;
}
A()
{
i++;
}
virtual ~A()
{
i--;
}
void fun()
{
cout << i<< " ";
}
static int i;
};
int A::i = 0;
void fun(A a) { // 如果是void fun(A &a)即引用,將不會呼叫拷貝建構函式,呼叫結束也就沒有析構了
static A aa; // A::i變成3,初始化1次,且整個程式執行結束時析構,如無static則退出fun函式就會析構
a.fun();
}
int main()
{
A *p = new A; // A::i變成1
A a; // A::i變成2
fun(a); // 會呼叫拷貝建構函式,呼叫之後會呼叫一次析構
fun(a); // 會呼叫拷貝建構函式,呼叫之後會呼叫一次析構
delete p; // 析構1次
fun(a); // 會呼叫拷貝建構函式,呼叫之後會呼叫一次析構
return 0;
}
輸出:
一、 建構函式是幹什麼的
class Counter
{
public:
// 類Counter的建構函式
// 特點:以類名作為函式名,無返回型別
Counter()
{
m_value = 0;
}
private:
// 資料成員
int m_value;
}
該類物件被建立時,編譯系統物件分配記憶體空間,並自動呼叫該建構函式->由建構函式完成成員的初始化工作
eg: Counter c1;
編譯系統為物件c1的每個資料成員(m_value)分配記憶體空間,並呼叫建構函式Counter( )自動地初始化物件c1的m_value值設定為0
故:
建構函式的作用:初始化物件的資料成員。
二、 建構函式的種類
class Complex
{
private :
double m_real;
double m_imag;
public:
// 無引數建構函式
// 如果建立一個類你沒有寫任何建構函式,則系統會自動生成預設的無參建構函式,函式為空,什麼都不做
// 只要你寫了一個下面的某一種建構函式,系統就不會再自動生成這樣一個預設的建構函式,如果希望有一個這樣的無參建構函式,則需要自己顯示地寫出來
Complex(void)
{
m_real = 0.0;
m_imag = 0.0;
}
// 一般建構函式(也稱過載建構函式)
// 一般建構函式可以有各種引數形式,一個類可以有多個一般建構函式,前提是引數的個數或者型別不同(基於c++的過載函式原理)
// 例如:你還可以寫一個 Complex( int num)的構造函數出來
// 建立物件時根據傳入的引數不同調用不同的建構函式
Complex(double real, double imag)
{
m_real = real;
m_imag = imag;
}
// 複製建構函式(也稱為拷貝建構函式)
// 複製建構函式引數為類物件本身的引用,用於根據一個已存在的物件複製出一個新的該類的物件,一般在函式中會將已存在物件的資料成員的值複製一份到新建立的物件中
// 若沒有顯示的寫複製建構函式,則系統會預設建立一個複製建構函式,但當類中有指標成員時,由系統預設建立該複製建構函式會存在風險,具體原因請查詢 有關 “淺拷貝” 、“深拷貝”的文章論述
Complex(const Complex & c)
{
// 將物件c中的資料成員值複製過來
m_real = c.m_real;
m_imag = c.m_imag;
}
// 型別轉換建構函式,根據一個指定的型別的物件建立一個本類的物件,
//需要注意的一點是,這個其實就是一般的建構函式,但是對於出現這種單引數的建構函式,C++會預設將引數對應的型別轉換為該類型別,有時候這種隱私的轉換是我們所不想要的,所以需要使用explicit來限制這種轉換。
// 例如:下面將根據一個double型別的物件建立了一個Complex物件
Complex(double r)
{
m_real = r;
m_imag = 0.0;
}
// 等號運算子過載(也叫賦值建構函式)
// 注意,這個類似複製建構函式,將=右邊的本類物件的值複製給等號左邊的物件,它不屬於建構函式,等號左右兩邊的物件必須已經被建立
// 若沒有顯示的寫=運算子過載,則系統也會建立一個預設的=運算子過載,只做一些基本的拷貝工作
Complex &operator=( const Complex &rhs )
{
// 首先檢測等號右邊的是否就是左邊的物件本身,若是本物件本身,則直接返回
if ( this == &rhs )
{
return *this;
}
// 複製等號右邊的成員到左邊的物件中
this->m_real = rhs.m_real;
this->m_imag = rhs.m_imag;
// 把等號左邊的物件再次傳出
// 目的是為了支援連等 eg: a=b=c 系統首先執行 b=c
// 然後執行 a= ( b=c的返回值,這裡應該是複製c值後的b物件)
return *this;
}
};
下面使用上面定義的類物件來說明各個建構函式的用法:
int main()
{
// 呼叫了無參建構函式,資料成員初值被賦為0.0
Complex c1,c2;
// 呼叫一般建構函式,資料成員初值被賦為指定值
Complex c3(1.0,2.5);
// 也可以使用下面的形式
Complex c3 = Complex(1.0,2.5);
// 把c3的資料成員的值賦值給c1
// 由於c1已經事先被建立,故此處不會呼叫任何建構函式
// 只會呼叫 = 號運算子過載函式
c1 = c3;
// 呼叫型別轉換建構函式
// 系統首先呼叫型別轉換建構函式,將5.2建立為一個本類的臨時物件,然後呼叫等號運算子過載,將該臨時物件賦值給c1
c2 = 5.2;
// 呼叫拷貝建構函式( 有下面兩種呼叫方式)
Complex c5(c2);
Complex c4 = c2; // 注意和 = 運算子過載區分,這裡等號左邊的物件不是事先已經建立,故需要呼叫拷貝建構函式,引數為c2
//這一點特別重要,這兒是初始化,不是賦值。其實這兒就涉及了C++中的兩種初始化的方式:複製初始化和賦值初始化。其中c5採用的是複製初始化,而c4採用的是賦值初始化,這兩種方式都是要呼叫拷貝建構函式的。
}
三、思考與測驗
1. 仔細觀察複製建構函式
Complex(const Complex & c)
{
// 將物件c中的資料成員值複製過來
m_real = c.m_real;
m_img = c.m_img;
}
為什麼函式中可以直接訪問物件c的私有成員?
答:(網上)因為拷貝建構函式是放在本身這個類裡的,而類中的函式可以訪問這個類的物件的所有成員,當然包括私有成員了。
2. 挑戰題,瞭解引用與傳值的區別
Complex test1(const Complex& c)
{
return c;
}
Complex test2(const Complex c)
{
return c;
}
Complex test3()
{
static Complex c(1.0,5.0);
return c;
}
Complex& test4()
{
static Complex c(1.0,5.0);
return c;
}
void main()
{
Complex a,b;
// 下面函式執行過程中各會呼叫幾次建構函式,呼叫的是什麼建構函式?
test1(a);
test2(a);
b = test3();
b = test4();
test2(1.2);
// 下面這條語句會出錯嗎?
test1(1.2); //test1( Complex(1.2 )) 呢?
}
答:
為了便於看建構函式的呼叫效果,我將類重新改一下,新增一些輸出資訊,程式碼如下:
#include <iostream>
using namespace std;
class Complex
{
private :
double m_real;
double m_imag;
int id;
static int counter;
public:
// 無引數建構函式
Complex(void)
{
m_real = 0.0;
m_imag = 0.0;
id=(++counter);
cout<<"Complex(void):id="<<id<<endl;
}
// 一般建構函式(也稱過載建構函式)
Complex(double real, double imag)
{
m_real = real;
m_imag = imag;
id=(++counter);
cout<<"Complex(double,double):id="<<id<<endl;
}
// 複製建構函式(也稱為拷貝建構函式)
Complex(const Complex & c)
{
// 將物件c中的資料成員值複製過來
m_real = c.m_real;
m_imag = c.m_imag;
id=(++counter);
cout<<"Complex(const Complex&):id="<<id<<" from id="<<c.id<<endl;
}
// 型別轉換建構函式,根據一個指定的型別的物件建立一個本類的物件
Complex(double r)
{
m_real = r;
m_imag = 0.0;
id=(++counter);
cout<<"Complex(double):id="<<id<<endl;
}
~Complex()
{
cout<<"~Complex():id="<<id<<endl;
}
// 等號運算子過載
Complex &operator=( const Complex &rhs )
{
if ( this == &rhs ) {
return *this;
}
this->m_real = rhs.m_real;
this->m_imag = rhs.m_imag;
cout<<"operator=(const Complex&):id="<<id<<" from id="<<rhs.id<<endl;
return *this;
}
};
int Complex::counter=0;
Complex test1(const Complex& c)
{
return c;
}
Complex test2(const Complex c)
{
return c;
}
Complex test3()
{
static Complex c(1.0,5.0);
return c;
}
Complex& test4()
{
static Complex c(1.0,5.0);
return c;
}
int main()
{
Complex a,b;
// 下面函式執行過程中各會呼叫幾次建構函式,呼叫的是什麼建構函式?
Complex c=test1(a);
Complex d=test2(a);
b = test3();
b = test4();
Complex e=test2(1.2);
Complex f=test1(1.2);
Complex g=test1(Complex(1.2));
return 0;
}
下面是程式執行結果:第一次執行結果:
第二次執行結果:
第三次執行結果:
四、附錄(淺拷貝與深拷貝)
上面提到,如果沒有自定義複製建構函式,則系統會建立預設的複製建構函式,但系統建立的預設複製建構函式只會執行“淺拷貝”,即將被拷貝物件的
資料成員的 值一一賦值給新建立的物件,若該類的資料成員中有指標成員,則會使得新的物件的指標所指向的地址與被拷貝物件的指標所指向的地址相同,
delete該指標 時則會導致兩次重複delete而出錯。下面是示例:
【淺拷貝與深拷貝】
#include <iostream.h>
#include <string.h>
class Person
{
public :
// 建構函式
Person(char * pN)
{
cout << "一般建構函式被呼叫 !\n";
m_pName = new char[strlen(pN) + 1];
//在堆中開闢一個記憶體塊存放pN所指的字串
if(m_pName != NULL)
{
//如果m_pName不是空指標,則把形參指標pN所指的字串複製給它
strcpy(m_pName ,pN);
}
}
// 系統建立的預設複製建構函式,只做位模式拷貝
Person(Person & p)
{
//使兩個字串指標指向同一地址位置
m_pName = p.m_pName;
}
~Person( )
{
delete m_pName;
}
private :
char * m_pName;
};
void main( )
{
Person man("lujun");
Person woman(man);
// 結果導致 man 和 woman 的指標都指向了同一個地址
// 函式結束析構時
// 同一個地址被delete兩次
}
// 下面自己設計複製建構函式,實現“深拷貝”,即不讓指標指向同一地址,而是重新申請一塊記憶體給新的物件的指標資料成員
Person(Person & chs);
{
// 用運算子new為新物件的指標資料成員分配空間
m_pName=new char[strlen(p.m_pName)+ 1];
if(m_pName)
{
// 複製內容
strcpy(m_pName ,chs.m_pName);
}
// 則新建立的物件的m_pName與原物件chs的m_pName不再指向同一地址了
}
下面討論一個重要問題是:建構函式的顯式呼叫
大家看看下面這段程式碼的輸出結果是什麼?這段程式碼有問題麼?
#include <iostream>
class CTest
{
public:
CTest()
{
m_a = 1;
}
CTest(int b)
{
m_b = b;
CTest();
}
~CTest()
{}
void show
{
std::cout << m_a << std::endl;
std::cout << m_b << std::endl;
}
private:
int m_a;
int m_b;
};
void main()
{
CTest myTest(2);
myTest.show();
}
-----------------------------------------------------------
【分析】
-----------------------------------------------------------
輸出結果中,m_a是一個不確定的值,因為沒有被賦初值,m_b 為2
注意下面這段程式碼
CTest(int b)
{
m_b = b;
CTest();
}
在呼叫CTest()函式時,實際上是建立了一個匿名的臨時CTest類物件,CTest()中賦值 m_a = 1 也是對該匿名物件賦值,故我們定義的myTest的m_a其實沒有被賦值。說白了,其實建構函式並不像普通函式那樣進行一段處理,而是建立了一個物件,並 且對該物件賦初值,所以顯式呼叫建構函式無法實現給私有成員賦值的目的。
這個例子告訴我們以後程式碼中千萬不要出現使用一個建構函式顯式呼叫另外一個建構函式,這樣會出現不確定性。其實一些初始化的程式碼可以寫在一個單獨的init函式中,然後每一個建構函式都呼叫一下該初始化函式就行了。
在此,順便再提出另外一個問題以供思考:
CTest *p = NULL;
void func()
{
p = new CTest();
}
程式碼右邊顯示呼叫CTest(),是否依然會產生一個匿名的臨時物件a,然後將該匿名的臨時物件a的地址賦給指標p? 如果是這樣的話,出了func函式後,臨時物件a是否會被析構? 那指標p不成為了野指標了?你能解釋這個問題麼?
答:我實驗的結果是不會產生臨時物件a,直接將產生的物件指標賦給了p
原文地址:https://www.cnblogs.com/xkfz007/archive/2012/05/11/2496447.html
相關推薦
C++建構函式詳解及顯式呼叫建構函式(explicit)
一. 什麼是拷貝建構函式 首先對於普通型別的物件來說,它們之間的複製是很簡單的,例如: int a = 100; int b = a; 而類物件與普通物件不同,類物件內部結構一般較為複雜,存在各種成員變數。 下面看一個類物件拷貝的簡單例子。 #include &
c++建構函式詳解及顯式呼叫建構函式
1>建構函式是幹什麼的 class Counter { public: Counter() {// 特點:以類名作為函式名,無返回型別 m_value = 0; } private: int m_value;// 資料成員 } 該類物件被
Activity詳解 Intent顯式跳轉和隱式跳轉, 及多個Activity之間傳值 總結
//web瀏覽器 Uri uri= Uri.parse("http://www.baidu.com:8080/image/a.jpg"); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(
Oracle中的substr()函式 詳解及應用
1)substr函式格式 (俗稱:字元擷取函式) 格式1: substr(string string, int a, int b); 格式2:substr(string string, int a) ; 解釋: 格式1:  
exec族函式詳解及迴圈建立子程序
前言:之前也知道exec族函式,但沒有完全掌握,昨天又重新學習了一遍,基本完全掌握了,還有一些父子程序和迴圈建立子程序的問題,還要介紹一下環境變數,今天分享一下。 一、環境變數 先介紹下環境的概念和特性,再舉例子吧。 環境變數,是指在作業系統中用來指定作業系統執行環境的一些引數。通常具備
opencv-python(cv2)影象二值化函式threshold函式詳解及引數cv2.THRESH_OTSU使用
cv2.threshold()函式的作用是將一幅灰度圖二值化,基本用法如下: #ret:暫時就認為是設定的thresh閾值,mask:二值化的影象 ret,mask = cv2.threshold(img2gray,175,255,cv2.THRESH_BINARY) pl
softmax + cross-entropy交叉熵損失函式詳解及反向傳播中的梯度求導
相關 正文 在大多數教程中, softmax 和 cross-entropy 總是一起出現, 求梯度的時候也是一起考慮. 我們來看看為什麼. 關於 softmax 和 cross-entropy 的梯度的求導過程, 已經在上面的兩篇文章中分別給出, 這裡
linux--fork()函式詳解及底層實現機制
fork底層實現機制:Linux中實現為呼叫clone函式,然後為do_fork,再然後copy_process()複製程序(複製相應資料結構例如:核心棧、thread_info、task_
spark三種清理資料的方式:UDF,自定義函式,spark.sql;Python中的zip()與*zip()函式詳解//及python中的*args和**kwargs
(1)UDF的方式清理資料 import sys reload(sys) sys.setdefaultencoding('utf8') import re import json from pyspark.sql import SparkSession
Oracle中的instr()函式 詳解及應用
1)instr()函式的格式 (俗稱:字元查詢函式) 格式一:instr( string1, string2 ) / instr(源字串, 目標字串) 格式二:instr( string1, string2 [, start_position [, nth_appearance ] ] ) /
C# sort 方法詳解及示例
諸如List<T>等泛型集合類,直接提供了sort()方法用於將集合中的元素進行排序。 但是,其前提是集合中存放的是可直接排序的基本型別,如List<int>, List<double>,如果 我們定義了一個自定義型別 Class MyClass,並建立一個自定義型別
java 浮點數表示詳解及解決方法(例項函式)
定點數表達法的缺點在於其形式過於僵硬,固定的小數點位置決定了固定位數的整數部分和小數部分,不利於同時表達特別大的數或者特別小的數。 計算機系統採納了所謂的浮點數表達方式。這種表達方式利用科學計數法來表達實數,即用一個尾數(Mantissa也叫有效數字 ),一個基數(Base
SetWindowPos函式詳解及CenterWindow()的用法
WinAPI: SetWindowPos - 改變視窗的位置與狀態 SetWindowPos( hWnd: HWND; {視窗控制代碼} hWndInsertAfter: HWND; {視窗的 Z 順序} X, Y: Integer; {位置} cx, cy: Inte
字串函式---itoa()函式詳解及實現
itoa()函式 itoa():char *itoa( int value, char *string,int radix); 原型說明: value:欲轉換的資料。 string:目標字串的地址。 radix:轉換後的進位制數,可以是10進位制、16進位制等,範圍必須在
【C#】氣泡排序、隱式和顯式轉換、函式及異常處理
一、普通氣泡排序: C#中常見的排序方法有:氣泡排序,快速排序,插入排序,選擇排序、堆排序以及歸併排序。雖然還沒學習過,但是也有耳聞,就先把它們先歸類。今天主要講這裡面最常見的氣泡排序。 【概念】 氣泡排序也就是講一組需要排序的數,進行從小到大,或從大到小的排列。計算機
C++繼承詳解之二——派生類成員函式詳解(函式隱藏、建構函式與相容覆蓋規則)
在這一篇文章開始之前,我先解決一個問題。 在上一篇C++繼承詳解之一——初探繼承中,我提到了在派生類中可以定義一個與基類成員函式同名的函式,這樣派生類中的函式就會覆蓋掉基類的成員函式。 在譚浩強的C++程式設計這本書第十一章,351頁最下面有這麼
C++中建構函式詳解
c++類的建構函式詳解 一、 建構函式是幹什麼的 class Counter { public: // 類Counter的建構函式 // 特點:
c++類詳解:訪問許可權,建構函式,拷貝建構函式,解構函式
類的定義 類可以看做是一種資料型別,類這種資料型別是一個包含成員變數和成員函式的集合。類的成員變數和普通變數一樣,也有資料型別和名稱,佔用固定長度的記憶體。但是,在定義類的時候不能對成員變數賦值,因為類只是一種資料型別或者說是一種模板,本身不佔用記憶體空間,而變數的值則需要
C++類中拷貝建構函式詳解
a. C++標準中提到“The default constructor, copy constructor and copy assignment operator, and destructor are special member functions.[Note: T