《C++Primer 第五版 中文版 》—— 第13章 拷贝控制
- 构造函数什么时候调用?
- 拷贝构造函数,移动构造函数什么时候调用?
- 拷贝赋值运算符、移动赋值运算符什么时候调用?
- 析构函数什么时候调用?
- 构造函数,拷贝构造函数,移动构造函数,拷贝赋值运算符,移动赋值运算符和析构函数的自定义和编译器合成关系是什么?
- 13.1 拷贝、赋值与销毁
- 如何定义拷贝构造函数?
- 拷贝构造函数在哪些情况下会被隐式使用?
- 如何定义合成拷贝构造函数?
- 合成拷贝构造函数的作用是什么?
- 拷贝初始化和直接初始化的区别?
- 什么是聚合类?
- 拷贝初始化的发生时机?
- 为什么拷贝构造函数的第一个参数必须是引用类型?
- 容器的初始化,insert元素,push元素, emplace元素,调用类的构造函数、移动构造函数还是拷贝构造函数?
- 如何理解:在拷贝初始化过程中,编译器可以(但不是必须)跳过拷贝/移动构造函数,直接创建对象。
- 什么是重载运算符?
- 如何定义拷贝赋值运算符?
- 如何定义合成拷贝赋值运算符?
- 合成拷贝赋值运算符的作用?
构造函数什么时候调用?
创建对象时。
拷贝构造函数,移动构造函数什么时候调用?
同类型对象初始化本对象时。
拷贝赋值运算符、移动赋值运算符什么时候调用?
同类型对象赋值给本对象时。
析构函数什么时候调用?
销毁对象时。
构造函数,拷贝构造函数,移动构造函数,拷贝赋值运算符,移动赋值运算符和析构函数的自定义和编译器合成关系是什么?
构造函数:
未自定义任何构造函数(自实现的默认构造函数,带参数的构造函数,拷贝构造函数,移动构造函数),编译器合成默认构造函数。
自定义任何一个构造函数(自实现的默认构造函数,带参数的构造函数,拷贝构造函数,移动构造函数),编译器不会合成默认构造函数。
拷贝构造函数:
用户定义了拷贝构造函数或移动构造函数,编译器不会合成拷贝构造函数。
用户未定义拷贝构造函数,编译器合成拷贝构造函数。
拷贝赋值运算符:
用户定义了拷贝赋值运算符或移动赋值运算符,编译器不会合成拷贝赋值运算符。
用户未定义拷贝赋值运算符,编译器合成拷贝赋值运算符。
移动构造函数:
用户定义了拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符或析构函数其中的一个,编译器不会合成默认移动构造函数。
用户未定义拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数,编译器合成默认移动构造函数。
移动赋值运算符:
用户定义了拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符或析构函数其中的一个,编译器不会合成默认移动赋值运算符。
用户未定义拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数,编译器合成默认移动赋值运算符。
析构函数:
用户定义了析构函数,编译器不会合成默认析构函数。
用户未定义析构函数,编译器合成默认析构函数。
13.1 拷贝、赋值与销毁
如何定义拷贝构造函数?
- 构造函数。
- 第一个参数是自身类类型 const 的引用,任何额外参数都有默认值。
#include <iostream>
using namespace std;class Foo
{
public:Foo(); //默认构造函数Foo(const Foo&);//拷贝构造函数
};int main()
{cout << "test run success" << endl;return 0;
}
拷贝构造函数在哪些情况下会被隐式使用?
TODO
如何定义合成拷贝构造函数?
没有为类自定义拷贝构造函数时,编译器会定义合成拷贝构造函数。
合成拷贝构造函数的作用是什么?
将参数对象的每个 非static 成员逐个拷贝到正在创建的对象中,每个成员类型决定了如何拷贝(类类型对象调用拷贝构造函数来拷贝;内置类型直接拷贝;数组逐元素拷贝,如果数组元素是类类型,使用元素的拷贝构造函数来拷贝。)。
拷贝初始化和直接初始化的区别?
string data(10, '.'); //直接初始化,调用构造函数。
string nines = string(100, '9');//拷贝初始化,调用拷贝构造函数或移动构造函数。
什么是聚合类?
- 没有用户提供的构造函数(没有定义任何构造函数,或者所有构造函数都有默认值)
- 没有 private 或 protected 的非静态数据成员,
- 没有虚函数。
- 没有基类。
struct MyAggregate {int a;double b;std::string c;
};
拷贝初始化的发生时机?
#include <iostream>
#include <string>class MyString
{
public:MyString(const char* s) : str(s){std::cout << "Constructing MyString from const char*: " << str << std::endl;}MyString(const MyString& other) : str(other.str){std::cout << "Copy constructing MyString: " << str << std::endl;}MyString(MyString&& other) noexcept : str(std::move(other.str)){std::cout << "Move constructing MyString: " << str << std::endl;}// Just for demonstrationconst std::string& get() const { return str; }private:std::string str;
};struct MyAggregate
{int a;double b;MyString c;
};void func1(MyString s1) {}MyString func2()
{MyString s2("test");return s2; //编译器优化,s2直接在函数调用的返回值(result)存储位置进行构造,不会调用拷贝构造函数和移动构造函数。
}int main()
{MyString arr[] = { "apple", "banana", "cherry" }; //花括号列表初始化数组中的元素,调用元素构造函数。func1(arr[0]); //实参传递给非引用类型形参,调用类拷贝构造函数。auto result = func2();MyAggregate obj = { 42, 3.14, "hello" }; //花括号列表初始化聚合类中的成员,s调用构造函数。return 0;
}
运行结果:
为什么拷贝构造函数的第一个参数必须是引用类型?
拷贝构造函数被用来初始化非引用类型对象。
如果拷贝构造函数不使用引用类型,为了调用拷贝构造函数,必须拷贝实参,拷贝实参又需要调用拷贝构造函数,如此会异常无限循环。
拷贝构造函数的第一个参数是引用类型,调用拷贝构造函数时不需要拷贝。
容器的初始化,insert元素,push元素, emplace元素,调用类的构造函数、移动构造函数还是拷贝构造函数?
#include <iostream>
#include <string>
#include <vector>
#include <list>
class MyString
{
public:MyString(const char* s = "") : str(s){std::cout << "Constructing MyString from const char*: " << str << std::endl;}MyString(const MyString& other) : str(other.str){std::cout << "Copy constructing MyString: " << str << std::endl;}MyString(MyString&& other) noexcept : str(std::move(other.str)){std::cout << "Move constructing MyString: " << str << std::endl;}// Just for demonstrationconst std::string& get() const { return str; }private:std::string str;
};int main()
{std::vector<MyString> vs1 = { "data1" }; //先使用 "data1" 构造 MyString 对象,然后容器初始化调用拷贝构造函数std::cout << "============================================================1" << std::endl;MyString data2("data2"); //使用 "data2" 构造 MyString 对象std::cout << "============================================================2" << std::endl;std::vector<MyString> vs2;std::cout << "============================================================3" << std::endl;vs2.push_back(data2); //push_back 调用拷贝构造函数std::cout << "============================================================4" << std::endl;std::vector<MyString> vs3;vs3.push_back("data3"); //先使用 "data3" 构造 MyString 对象,再push_back调用移动构造函数std::cout << "============================================================5" << std::endl;MyString data4("data4");std::cout << "============================================================6" << std::endl;std::list<MyString> ls;ls.insert(ls.begin(), data4);//insert 调用拷贝构造函数std::cout << "============================================================7" << std::endl;ls.emplace_back("data5"); //emplace_back 调用构造函数std::cout << "============================================================8" << std::endl;ls.insert(ls.begin(), "data6");//先使用 "data6" 构造 MyString 对象,再insert调用移动构造函数std::cout << "============================================================9" << std::endl;return 0;
}
运行结果:
如何理解:在拷贝初始化过程中,编译器可以(但不是必须)跳过拷贝/移动构造函数,直接创建对象。
#include <iostream>
#include <string>
#include <vector>
#include <list>class MyString
{
public:MyString(const char* s = "") : str(s) {std::cout << "Constructing MyString from const char*: " << str << std::endl;}// Just for demonstrationconst std::string& get() const { return str; }private:std::string str;MyString(const MyString& other) : str(other.str){std::cout << "Copy constructing MyString: " << str << std::endl;}MyString(MyString&& other) noexcept : str(std::move(other.str)){std::cout << "Move constructing MyString: " << str << std::endl;}};int main()
{MyString data2 = "data2";//调用构造函数//说明:拷贝构造和移动构造 是private 无影响return 0;
}
运行结果:
什么是重载运算符?
本质是函数。
函数名:operator[要重载的运算符符号]。
参数列表:运算对象。
返回类型:和内置类型一致。
如何定义拷贝赋值运算符?
class Foo
{
public:Foo& operator=(const Foo&); //赋值运算符//返回指向左侧运算对象的引用,和内置类型的赋值保持一致。
};
如何定义合成拷贝赋值运算符?
如果一个类未自定义拷贝赋值运算符,编译器生成一个合成拷贝赋值运算符。
合成拷贝赋值运算符的作用?
将右侧运算对象每个非static成员赋予左侧运算对象的对应成员。
类对象成员调用拷贝赋值运算符,基础类型直接赋值。数组类型的成员逐个赋值数组元素。
返回一个指向左侧运算对象的引用。