原来 C++
类中,有 6
个默认成员函数:
最后重要的是前 4
个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
而 C++11
新增了两个:移动构造函数和移动赋值运算符重载。
针对 移动构造函数 和 移动赋值运算符重载 有一些需要注意的点如下:
C++11
允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这个我们在类和对象这里就不再细讲了。
注意这只是声明,不是定义!如果在构造函数中没有给该成员变量赋值的话,那么才会采用这个初始缺省值!
下面举个例子:
class A
{
public:
// ...
private:
int x = 10; // 声明缺省值
string str = "liren";
const float ft = 10.3;
};
C++11
可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default
关键字显示指定移动构造生成。
default
只能修饰类中默认提供的成员函数:无参构造函数、拷贝构造函数、赋值运算符重载函数、析构函数,且该特殊成员函数没有默认参数=default
函数既可以在类内定义, 也可以在类外定义class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{ // 实现省略 }
Person(const Person& p) // 有了拷贝构造,不会默认生成移动构造
:_name(p._name)
,_age(p._age)
{ // 实现省略 }
Person(Person&& p) = default; // 所以利用=default强制使用默认的移动构造
int f() = default; // ❌f不是Person的特殊成员函数
private:
liren::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
//运行结果
string(const string& s) -- 深拷贝
string(string&& s) -- 移动资源
如果能想要 限制某些默认函数的生成,在 C++98
中,是将该函数设置成 private
,并且只声明但不实现,这样只要其他人想要调用就会报错。在 C++11
中更简单,只需在该函数声明加上 =delete 即可,该语法指示编译器不生成对应函数的默认版本,称 =delete
修饰的函数为删除函数。
🔴 注意在 C++98
中要实现这个目的,一定要两个条件都满足:①将该函数设置成private
②只声明不实现
因为如果我们只是将函数设置为 private
的话,那么其实我们照样还是可以通过类内的其他函数进行生成该函数的,因为类内的访问是不受限制的;如果我们只声明不实现,但是不设置为 private
,虽然说没有实现的内容,相当于什么都没做,但是有一个漏洞,就是可以在类外实现,这样子的话就还是会出现被生成的可能。
所以在 C++98
中要同时满足这两个条件才能达到目的!
♻️ 注意:避免删除函数和 explicit
一起使用
delete
常用于要使用单例模式的时候,要求只能生成一个唯一的对象(这个后面会讲如何实现),我们就可以让构造函数加上 delete
,这样子的话就不能直接进行构造对象了!
还有就是比如我们要让一个类A的对象不允许拷贝构造和赋值,那么我们就可以让其拷贝构造和赋值重载加上 delete
即可达到目的!
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;
//void* operator new(size_t size){} //重载new运算符
void* operator new(size_t size)=delete; //重载new运算符,此函数被禁用
//void* operator new[](size_t size){} //重载new[]运算符
void* operator new[](size_t size)=delete; //重载new[]运算符,此函数被禁用
private:
liren::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
🔺 注意:这里只要声明了拷贝构造或者赋值重载或者析构函数是用 delete
的话,说明不使用默认生成的,则代表编译器就不会默认生成移动构造了~
C++
对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来 debug
会得不偿失,因此:C++11
提供了 override
和 final
两个关键字,可以帮助用户检测是否重写 和 禁用一个类的进一步衍生。
final
:修饰类时,表示该类不能被继承;修饰派生类的虚函数时,表示该虚函数不能被子类继承或重写class Car
{
public:
virtual void Drive() final {} // 在不想被继承的函数之前加上final
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
🚩 运行结果:
error C3248: “Car::Drive”: 声明为“final”的函数无法被“Benz::Drive”重写
override
: 在编译阶段检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。注意 override
只能修饰子类的虚函数class Car
{
public:
virtual void Drive(char ch) {}
};
class Benz :public Car
{
public:
// 在想检测的虚函数的实现之前加上override
virtual void Drive(int i) override {cout << "Benz-舒适" << endl;}
};
🚩 运行结果:
error C3668: “Benz::Drive”: 包含重写说明符“override”的方法没有重写任何基类方法
子类 为完成基类初始化,在 C++11
之前,需要在初始化列表调用基类的构造函数,从而完成构造函数的传递。如果基类拥有多个构造函数,那么子类也需要实现多个与基类构造函数对应的构造函数。
下面举个例子:
class Base {
public:
// 基类拥有多个构造函数
Base(int v) : _value(v), _c('0') {}
Base(char c) : _value(0), _c(c) {}
private:
int _value;
char _c;
};
class Derived : public Base {
public:
// 初始化基类需要透传参数至基类的各个构造函数,非常麻烦
Derived(int v) : Base(v) {}
Derived(char c) : Base(c) {}
// 假设派生类只是添加了一个普通的函数
void display() {
// dosomething
}
};
书写多个派生类 构造函数 只为传递参数完成基类初始化,这种方式无疑给开发人员带来麻烦,降低了编码效率。从 C++11
开始,推出了继承构造函数(Inheriting Constructor),使用 using
来声明继承基类的构造函数,我们可以这样书写。
class Base {
public:
// 基类拥有多个构造函数
Base(int v) : _value(v), _c('0') {}
Base(char c) : _value(0), _c(c) {}
private:
int _value;
char _c;
};
class Derived : public Base {
public:
// 使用using来继承父类的构造函数
using Base::Base;
// 假设派生类只是添加了一个普通的函数
void display() {
// dosomething
}
};
我们通过 using Base::Base
把基类构造函数继承到派生类中,不再需要书写多个派生类构造函数来完成基类的初始化,非常的方便!
更为巧妙的是,C++11
标准规定,继承构造函数与类的一些默认函数(默认构造、析构、拷贝构造函数等)一样,是隐式声明,如果一个继承构造函数不被相关代码使用,编译器不会为其产生真正的函数代码。这样比通过派生类构造函数 “透明传递构造函数参数” 来完成基类初始化的方式,总是需要定义派生类的各种构造函数更加节省目标代码空间。
这个很好理解,因为继承构造函数的功能是初始化基类,对于派生类数据成员的初始化则无能为力。解决的办法主要有两个:
一是使用 C++11
特性就地初始化成员变量,可以通过 =
、{}
对非静态成员快速就地初始化,以减少多个构造函数重复初始化变量的工作,注意初始化列表会覆盖就地初始化操作!
class Derived : public Base {
public:
using Base::Base;
void display() {
// dosomething
}
private:
// 派生类新增数据成员并声明缺省值
double _d = 10.0;
};
二是新增派生类构造函数,使用构造函数初始化列表。
class Derived : public Base {
public:
using Base::Base;
// 新增派生类构造函数
Derived(int a, double d) : Base(a), _d(d) {}
void display() {
// dosomething
}
private:
// 派生类新增数据成员并声明缺省值
double _d = 10.0;
};
相比之下,第二种方法需要新增构造函数,明显没有第一种方法简洁,但第二种方法可由用户控制初始化值,更加灵活。各有优劣,两种方法需结合具体场景使用!
class A {
public:
A(int a = 3, double b = 4) : _a(a), _b(b) {}
void display() {
// do something...
}
private:
int _a;
double _b;
};
class B :public A {
public:
using A::A;
};
那么 A 中的构造函数会有下面几个版本:
A()
A(int)
A(int, double)
A(const A&)
那么 B
中对应的继承构造函数将会有如下几个版本:
B()
B(int)
B(int, double)
B(const B&)
可以看出,参数默认值会导致多个构造函数版本的产生,因此在使用时需格外小心。
class A {
public:
A(int i) {}
};
class B {
public:
B(int i) {}
};
class C : public A, public B {
public:
using A::A;
using B::B; //编译出错,重复定义C(int)
// 显示定义继承构造函数 C(int)
C(int i) :A(i), B(i) {}
};
为避免继承构造函数冲突,可以通过显示定义来阻止隐式生成的继承构造函数。
此外,使用继承构造函数还需要注意:如果基类构造函数被申明为私有成员函数,或者派生类是从虚基类继承而来 ,那么就不能在派生类中申明继承构造函数。
委托构造函数其实就是当我们一个类中有不同的构造函数,而其中一个版本的构造函数中实现的一部分和另一个版本的构造函数是一模一样的,那么此时我们可以让这个版本的构造函数委托于另一个版本的构造函数,其目的也是为了减少程序员书写构造函数的时间!
举个例子:
class entrust
{
public:
entrust() {}
entrust(int f)
{
cout << "this is first " << f << endl;
}
entrust(int f, int s)
{
cout << "this is first " << f << endl;
cout << "this is second " << s << endl;
}
entrust(int f, int s, int t)
{
cout << "this is first " << f << endl;
cout << "this is second " << s << endl;
cout << "this is third " << t << endl;
}
private:
int _first;
int _second;
int _third;
};
可以看到上述代码中其实 entrust(int first, int second, int third)
是包含了前两个构造函数的内容的,但是我们都得重复的描述出来,这就显得非常的麻烦!(这里只是举个例子,如果在大型工程中可能有数万行)
所以我们可以通过添加一个执行所有验证的函数来减少重复的代码,但是如果一个构造函数可以将部分工作委托给其他构造函数,则 entrust
的代码更易于了解和维护。 若要添加委托构造函数,请使用 constructor (. . .) : constructor (. . .)
语法,所以我们可以尝试写成以下的形式:
class entrust
{
public:
entrust() {}
entrust(int f)
{
cout << "this is first " << f << endl;
}
entrust(int f, int s) : entrust(f)
{
cout << "this is second " << s << endl;
}
entrust(int f, int s, int t) : entrust(f, s)
{
cout << "this is third " << t << endl;
}
private:
int _first;
int _second;
int _third;
};
int main()
{
entrust et(1, 2, 3);
return 0;
}
// 运行结果
this is first 1
this is second 2
this is third 3
上述代码中,我们在 main 函数中调用了 entrust
的三个参数版本的构造函数也就是 entrust(int f, int s, int t)
,而它可以委托于 entrust(int f, int s)
,而 entrust(int f, int s)
又委托于 entrust(int f)
,这就形成了类似堆栈的一个思路,并且从打印结果来看,entrust(int f)
是最先被调用的,也就是说 每次的那个被委托的构造函数是先被执行的,最后再返回来执行原函数的内容!
除此之外,每个构造函数将仅执行其他构造函数不会执行的工作。
❤ 委托构造函数有以下一些实现注意:
class Test {
public:
Test() {}
// 此处为成员初始化,没有委托
Test(string str) : _str(str) {}
// can't do member initialization here
// error C3511: a call to a delegating constructor shall be the only member-initializer
Test(string str, double dbl) : Test(str), _dbl{ dbl } {} // ❌
// 若仅仅是成员赋值而不是初始化则可以
Test(string str, double dbl) : Test(str) { _dbl = dbl; } // ✔
private:
double _dbl = 1.0;
string _str;
};
class Test {
public:
Test() {}
Test(string str) : _str(str) {}
Test(string str, double dbl) : Test(str) { _dbl = dbl; }
void print() { cout << _dbl << '\n' << _str << endl; }
private:
double _dbl = 1.0; // 声明缺省值为1.0
string _str;
};
int main()
{
Test t{ "liren", 2.0 };
t.print();
}
Constructor1
将调用 Constructor2
(其调用 Constructor1
),在出现堆栈溢出之前不会出错,我们应当避免该循环class Test {
public:
// ❌don't do this
Test() : Test(6, 3) { }
Test(int my_max, int my_min) : Test() { }
private:
int max;
int min;
};