一 拷貝構造函數的概念:
拷貝構造函數是一種特殊的構造函數,用于創建一個對象是另一個對象的副本。當需要用一個已存在的對象來初始化一個新對象時,或者將對象傳遞給函數或從函數返回對象時,會調用拷貝構造函數。
二?拷貝構造函數的特點:
1:拷貝構造函數是構造函數的一個重載形式。
2:拷貝構造函數的參數只有一個且必須是類類型對象的引用,使用傳值方式編譯器直接報錯, 因為會引發無窮遞歸調用。
3:若未顯式定義,編譯器會生成默認的拷貝構造函數。 默認的拷貝構造函數對象按內存存儲按 字節序完成拷貝,這種拷貝叫做淺拷貝,或者值拷貝。
注意:在編譯器生成的默認拷貝構造函數中,內置類型是按照字節方式直接拷貝的,而自定 義類型是調用其拷貝構造函數完成拷貝的。
4:編譯器生成的默認拷貝構造函數已經可以完成字節序的值拷貝了,還需要自己顯式實現嗎? 當然像日期類這樣的類是沒必要的。
2.1 代碼示例:
class Time
{
public:// 普通構造函數Time(int hour = 0, int minute = 0, int second = 0) {_hour = hour;_minute = minute;_second = second;}// 拷貝構造函數,使用引用傳遞Time(const Time& other) {_hour = other._hour;_minute = other._minute;_second = other._second;}void Print() const {std::cout << _hour << ":" << _minute << ":" << _second << std::endl;}private:int _hour;int _minute;int _second;
};int main()
{Time t1(10, 20, 30); // 使用普通構造函數//構造函數的重載Time t2 = t1; // 使用拷貝構造函數//Time t2(t1); // 拷貝構造的另一種寫法t1.Print();t2.Print();return 0;
}
輸出:
2.2 為什么要使用引用呢?
我們在 increment 函數中改變x的值并沒有間接性改變a,這是因為傳過去的只是編譯器創建實參的一個副本,而修改副本怎么可能可以改變a呢?
#include <iostream>void increment(int x)
{x = x + 1; // 修改的是副本,不影響實參
}int main()
{int a = 5;increment(a); // 傳遞a的副本std::cout << a << std::endl; // 輸出5,原始值a未被修改return 0;
}
知道傳值傳參的本質之后,再來想一想為什么要用引用?咱們先來說說如果沒用用引用的后果會是怎么樣,當把自定義類型傳出去后且不用引用或者指針來接收,它會
調用 Time(const Time other)
,其中 other
是 t1
的按值傳遞副本。
為了按值傳遞,編譯器需要創建 other
的副本。
創建 other
的副本時,再次調用 Time(const Time other)
。
這個新調用的 Time(const Time other)
又需要創建自己的 other
副本,再次調用 Time(const Time other)
。
如此反復,導致無限遞歸調用,最終導致棧溢出。
圖:
C++規定,自定義類型的拷貝,都會調用拷貝構造
那為什么要引用呢?
首先我們來回顧一下引用 :
1:引用是現有變量的另一個名字。
2:它們不創建新對象,只是指向已有對象。
3:引用只是指向現有對象,不創建新副本
因為引用就是它本身,所以何來創建新副本這一說法,創建新副本是怕改變副本從而導致改變實參值
2.3 總結:
1:按值傳遞會遞歸:每次傳遞對象會復制對象,導致無限遞歸。
2:引用傳遞避免遞歸:引用只是指向對象本身,不會復制對象
三 默認拷貝構造:
當你沒有顯式定義拷貝構造函數時,編譯器會為你自動生成一個默認的拷貝構造函數。這個默認拷貝構造函數會逐個拷貝對象的所有成員變量。
3.1 內置類型與自定義類型的拷貝:
內置類型:如 int
, char
, float
等,拷貝時直接按照字節方式進行復制,也就是直接復制其值。
自定義類型:如類和結構體,拷貝時會調用該類型的拷貝構造函數。
3.2 代碼示例:
內置類型:
#include <iostream>class MyClass
{
public:int x; // 內置類型成員
};int main()
{MyClass obj1;obj1.x = 10;MyClass obj2 = obj1; // 使用編譯器生成的默認拷貝構造函數std::cout << "obj1.x: " << obj1.x << std::endl; std::cout << "obj2.x: " << obj2.x << std::endl;return 0;
}
輸出:
對于一個類里面只有內置類型成員那編譯器生成的默認拷貝構造會自動復制其值。
自定義類型:
#include <iostream>class Time
{
public:// 默認構造函數Time() { _hour = 0;_minute = 0;_second = 0;}// 拷貝構造函數Time(const Time& other) {_hour = other._hour;_minute = other._minute;_second = other._second;std::cout << "Time::Time(const Time& other)" << std::endl;}private:int _hour;int _minute;int _second;
};class MyClass
{
public:int x; // 內置類型成員Time t; // 自定義類型成員
};int main()
{MyClass obj1;obj1.x = 10;MyClass obj2 = obj1; // 使用編譯器生成的默認拷貝構造函數std::cout << "obj1.x: " << obj1.x << std::endl;std::cout << "obj2.x: " << obj2.x << std::endl; return 0;
}
當執行MyClass obj2 = obj1; 因obj1類里面有自定義類型 t 所以編譯器生成的默認拷貝構造會自動調用Time(const Time& other) 來完成
3.3 總結:
內置類型:編譯器默認拷貝構造函數會直接復制其值。
自定義類型:編譯器默認拷貝構造函數會調用該類型的拷貝構造函數來復制其內容。
四 內存分區:
要理解好深拷貝與淺拷貝那就得先了解內存是怎么樣分區的。
計算機程序運行時,內存通常被分為四個主要區域:棧區、堆區、全局靜態區和只讀區(常量區和代碼區)。
4.1 棧區:
局部變量:函數內部定義的變量。
形參(函數參數):函數定義時的參數。
返回地址:函數調用后的返回地址。
特點:
棧區中訪問速度快且棧的內存連續分配。
因存儲的都是 局部/形參/返回地址 所以棧區空間小,存儲的生命周期短。
在我們局部變量所在的函數執行完成時,它會自動釋放內存。
4.2 堆區:
動態分配的數據:通過 new
或 malloc
等動態分配函數分配的內存。
特點:
因存儲的都是new 或者malloc開辟的空間所以堆區空間大,所以訪問速度慢。
堆中的內存分配和釋放是通過指針進行的,可能不是連續的。
堆區的內存需要程序員手動管理,必須手動釋放動態分配的內存,否則會導致內存泄漏。
4.3 全區/靜態區:
全局變量:在所有函數外部定義的變量。
靜態變量:使用 static
關鍵字定義的變量。
特點:
全局變量和靜態變量在程序的整個運行期間一直存在,直到程序結束。
全局變量可以在程序的所有函數中訪問,靜態變量在聲明的作用域內共享。
4.4 只讀常量區:
常量:程序中定義的常量。
代碼:程序的指令代碼。
特點:
常量區的數據在程序運行期間不能被修改,保證了數據的安全性和穩定性。
代碼區存儲程序的指令代碼,在程序運行時被載入內存以執行。
五 淺拷貝:
首先我們來回顧C語言里面的基本類型和指針類型。
5.1 基本類型:
基本類型是C語言內置的數據類型,它們用于存儲最基本的數值數據。常見的基本類型包括:int float char……
5.2 指針類型:
指針類型是存儲內存地址的數據類型。指針用于指向其他變量或對象在內存中的位置。
5.3 基本類型代碼示例:
#include <iostream>class BasicType
{
public:int value;// 構造函數BasicType(int v) {value = v;}// 拷貝構造函數BasicType(const BasicType& other) {value = other.value;}
};int main()
{BasicType obj1(10);BasicType obj2 = obj1; // 淺拷貝,復制基本類型的值std::cout << "改變前: " << std::endl;std::cout << "obj1.value: " << obj1.value << std::endl;std::cout << "obj2.value: " << obj2.value << std::endl;obj2.value = 20; // 修改obj2的值std::cout << "改變后: " << std::endl;std::cout << "obj1.value: " << obj1.value << std::endl;std::cout << "obj2.value: " << obj2.value << std::endl;return 0;
}
輸出:
值會被復制但修改新對象的值不會影響原對象。
5.3 指針類型代碼示例:
#include <iostream>class SimplePointer
{
public:int* ptr; // 成員變量 ptr// 構造函數SimplePointer(int value)
{ptr = (int*)malloc(sizeof(int)); // 動態分配內存并初始化if (ptr != nullptr) {*ptr = value;}
}SimplePointer(const SimplePointer& other) {this->ptr = other.ptr; // 淺拷貝,復制內存地址}void print() const {std::cout << "Value: " << *ptr << std::endl;}
};int main()
{SimplePointer obj1(10); // 創建第一個對象,并將值初始化為10SimplePointer obj2(obj1); // 使用拷貝構造函數(淺拷貝)// 打印初始值std::cout << "Initial values:" << std::endl;obj1.print();obj2.print();// 修改obj2的值*obj2.ptr = 20;// 打印修改后的值std::cout << "After change:" << std::endl;obj1.print();obj2.print(); return 0;
}
輸出:
復制內存地址,共享同一塊內存,修改會互相影響
六 深拷貝:
#include <iostream>
#include <cstdlib>
#include <cstring>class SimpleClass
{
public:int* ptr;// 默認構造函數SimpleClass(int value) {ptr = (int*)malloc(sizeof(int)); // 動態分配內存并初始化if (ptr != nullptr) {*ptr = value;}}// 深拷貝構造函數SimpleClass(const SimpleClass& other) {ptr = (int*)malloc(sizeof(int)); // 分配新內存if (ptr != nullptr) {*ptr = *(other.ptr); // 復制內容}}// 析構函數~SimpleClass() {if (ptr != nullptr) {free(ptr); // 釋放內存}}void Print() const {if (ptr != nullptr) {std::cout << "Value: " << *ptr << std::endl;}}
};int main()
{SimpleClass obj1(10); // 創建對象,ptr 指向的值為 10SimpleClass obj2 = obj1; // 使用深拷貝構造函數obj1.Print();obj2.Print();// 修改 obj2 的值if (obj2.ptr != nullptr) {*(obj2.ptr) = 20;}obj1.Print();obj2.Print();return 0;
}
輸出:
深拷貝不僅復制對象的指針成員,還為指針指向的內容分配新的內存,并復制原對象的數據。這樣,兩個對象擁有獨立的內存,修改一個不會影響另一個。