前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【C++11】新的类功能

【C++11】新的类功能

作者头像
利刃大大
发布2025-03-12 08:31:20
发布2025-03-12 08:31:20
6800
代码可运行
举报
文章被收录于专栏:csdn文章搬运csdn文章搬运
运行总次数:0
代码可运行

Ⅰ. 默认成员函数

原来 C++ 类中,有 6 个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载(了解即可)
  6. const 取地址重载(了解即可)

最后重要的是前 4 个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的

C++11 新增了两个:移动构造函数和移动赋值运算符重载。

针对 移动构造函数移动赋值运算符重载 有一些需要注意的点如下:

  • 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
  • 如果你 提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值

Ⅱ. 类成员变量初始化

C++11 允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这个我们在类和对象这里就不再细讲了。

注意这只是声明,不是定义!如果在构造函数中没有给该成员变量赋值的话,那么才会采用这个初始缺省值!

​ 下面举个例子:

代码语言:javascript
代码运行次数:0
运行
复制
class A
{
public:
    // ...
private:
    int x = 10;  // 声明缺省值
    string str = "liren";
    const float ft = 10.3;
};

Ⅲ. 强制生成默认函数的关键字 default

C++11 可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显示指定移动构造生成。

  • default 只能修饰类中默认提供的成员函数:无参构造函数、拷贝构造函数、赋值运算符重载函数、析构函数,且该特殊成员函数没有默认参数
  • =default 函数既可以在类内定义, 也可以在类外定义
代码语言:javascript
代码运行次数:0
运行
复制
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) -- 移动资源

Ⅳ. 禁止生成默认函数的关键字 delete

​ 如果能想要 限制某些默认函数的生成,在 C++98 中,是将该函数设置成 private并且只声明但不实现,这样只要其他人想要调用就会报错。在 C++11 中更简单,只需在该函数声明加上 =delete 即可,该语法指示编译器不生成对应函数的默认版本,称 =delete 修饰的函数为删除函数。

🔴 注意在 C++98 中要实现这个目的,一定要两个条件都满足:①将该函数设置成private ②只声明不实现

​ 因为如果我们只是将函数设置为 private 的话,那么其实我们照样还是可以通过类内的其他函数进行生成该函数的,因为类内的访问是不受限制的;如果我们只声明不实现,但是不设置为 private,虽然说没有实现的内容,相当于什么都没做,但是有一个漏洞,就是可以在类外实现,这样子的话就还是会出现被生成的可能。

​ 所以在 C++98 中要同时满足这两个条件才能达到目的!

♻️ 注意:避免删除函数和 explicit 一起使用

delete 常用于要使用单例模式的时候,要求只能生成一个唯一的对象(这个后面会讲如何实现),我们就可以让构造函数加上 delete,这样子的话就不能直接进行构造对象了!

​ 还有就是比如我们要让一个类A的对象不允许拷贝构造和赋值,那么我们就可以让其拷贝构造和赋值重载加上 delete 即可达到目的!

代码语言:javascript
代码运行次数:0
运行
复制
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 的话,说明不使用默认生成的,则代表编译器就不会默认生成移动构造了~

Ⅴ. final 与 override 关键字

C++ 对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来 debug 会得不偿失,因此:C++11 提供了 overridefinal 两个关键字,可以帮助用户检测是否重写 和 禁用一个类的进一步衍生

  1. final :修饰类时,表示该类不能被继承;修饰派生类的虚函数时,表示该虚函数不能被子类继承或重写
代码语言:javascript
代码运行次数:0
运行
复制
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”重写
  1. override 在编译阶段检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。注意 override 只能修饰子类的虚函数
代码语言:javascript
代码运行次数:0
运行
复制
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 之前,需要在初始化列表调用基类的构造函数,从而完成构造函数的传递。如果基类拥有多个构造函数,那么子类也需要实现多个与基类构造函数对应的构造函数

​ 下面举个例子:

代码语言:javascript
代码运行次数:0
运行
复制
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 来声明继承基类的构造函数,我们可以这样书写。

代码语言:javascript
代码运行次数:0
运行
复制
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 标准规定,继承构造函数与类的一些默认函数(默认构造、析构、拷贝构造函数等)一样,是隐式声明如果一个继承构造函数不被相关代码使用,编译器不会为其产生真正的函数代码。这样比通过派生类构造函数 “透明传递构造函数参数” 来完成基类初始化的方式,总是需要定义派生类的各种构造函数更加节省目标代码空间。

注意事项

  1. 如果基类的构造函数被声明为私有,或者派生类是从基类中虚继承,那么不能继承构造函数
  2. 一旦使用继承构造函数,编译器不会再为派生类生成默认构造函数
  3. 继承的构造函数只能初始化基类中的成员变量,不能初始化派生类的成员变量

​ 这个很好理解,因为继承构造函数的功能是初始化基类,对于派生类数据成员的初始化则无能为力。解决的办法主要有两个:

​ 一是使用 C++11 特性就地初始化成员变量,可以通过 ={} 对非静态成员快速就地初始化,以减少多个构造函数重复初始化变量的工作,注意初始化列表会覆盖就地初始化操作

代码语言:javascript
代码运行次数:0
运行
复制
class Derived : public Base {
public:
    using Base::Base;

    void display() {
        // dosomething		
    }
private:
    // 派生类新增数据成员并声明缺省值
    double _d = 10.0;
};

​ 二是新增派生类构造函数,使用构造函数初始化列表。

代码语言:javascript
代码运行次数: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;
};

​ 相比之下,第二种方法需要新增构造函数,明显没有第一种方法简洁,但第二种方法可由用户控制初始化值,更加灵活。各有优劣,两种方法需结合具体场景使用!

  1. 构造函数拥有默认值会产生多个构造函数版本,且继承构造函数无法继承基类构造函数的默认参数,所以我们在使用有默认参数构造函数的基类时必须要小心
代码语言:javascript
代码运行次数: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 中的构造函数会有下面几个版本:

代码语言:javascript
代码运行次数:0
运行
复制
A()
A(int)
A(int, double)
A(const A&)

​ 那么 B 中对应的继承构造函数将会有如下几个版本:

代码语言:javascript
代码运行次数:0
运行
复制
B()
B(int)
B(int, double)
B(const B&)

​ 可以看出,参数默认值会导致多个构造函数版本的产生,因此在使用时需格外小心。

  1. 多继承的情况下,继承构造函数会出现 “冲突” 的情况,因为多个基类中的部分构造函数可能导致派生类中的继承构造函数的函数签名(函数名与参数)相同
代码语言:javascript
代码运行次数:0
运行
复制
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) {}
};

​ 为避免继承构造函数冲突,可以通过显示定义来阻止隐式生成的继承构造函数。

​ 此外,使用继承构造函数还需要注意:如果基类构造函数被申明为私有成员函数,或者派生类是从虚基类继承而来 ,那么就不能在派生类中申明继承构造函数。

Ⅶ. 委托构造

​ 委托构造函数其实就是当我们一个类中有不同的构造函数,而其中一个版本的构造函数中实现的一部分和另一个版本的构造函数是一模一样的,那么此时我们可以让这个版本的构造函数委托于另一个版本的构造函数,其目的也是为了减少程序员书写构造函数的时间

​ 举个例子:

代码语言:javascript
代码运行次数:0
运行
复制
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 (. . .) 语法,所以我们可以尝试写成以下的形式:

代码语言:javascript
代码运行次数:0
运行
复制
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) 是最先被调用的,也就是说 每次的那个被委托的构造函数是先被执行的,最后再返回来执行原函数的内容

​ 除此之外,每个构造函数将仅执行其他构造函数不会执行的工作

❤ 委托构造函数有以下一些实现注意:

  • 调用的第一个构造函数将初始化对象,以便此时初始化其所有成员。 不能在委托给另一个构造函数的构造函数中执行成员初始化
代码语言:javascript
代码运行次数:0
运行
复制
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;
};
  • 在非静态数据成员初始值设定项的使用过程中,如果构造函数还将初始化给定数据成员,则将重写成员初始值设定项
代码语言:javascript
代码运行次数:0
运行
复制
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),在出现堆栈溢出之前不会出错,我们应当避免该循环
代码语言:javascript
代码运行次数:0
运行
复制
class Test {
public:
    // ❌don't do this
    Test() : Test(6, 3) { }  
    Test(int my_max, int my_min) : Test() { }
private:
    int max;
    int min;
};
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Ⅰ. 默认成员函数
  • Ⅱ. 类成员变量初始化
  • Ⅲ. 强制生成默认函数的关键字 default
  • Ⅳ. 禁止生成默认函数的关键字 delete
  • Ⅴ. final 与 override 关键字
  • Ⅵ. 继承构造函数
    • 使用方法
    • 注意事项
  • Ⅶ. 委托构造
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档