C++基础总结(下)

本文深入探讨了C++的基础概念,包括拷贝构造函数的定义、触发条件和注意事项,析构函数的特点、触发条件及其在对象生命周期中的角色。此外,还介绍了对象移动的概念,如右值引用、移动语义和完美转发。最后,文章涵盖了运算符重载、面向对象编程、模板与泛式编程等核心话题。
摘要由CSDN通过智能技术生成


拷贝控制


一、拷贝构造函数

1.定义

  • 一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,此构造函数是拷贝构造函数
  • 合成拷贝构造函数是缺省的拷贝构造函数(自己未定义,编译器自动生成的)可以逐元素地拷贝一个数组类型的成员
//逐元素拷贝,但却是浅拷贝,下面是浅拷贝的例子
#include <iostream>
#include<string>

using namespace std;

class B
{
public:
    int y;

    B(int y) : y(y)
    {

    }
};

class A
{
public:
    B *x[20];

    A()
    {
        x[0] = new B(2);
    }
};

int main()
{
    A a;
    A b = a;

    cout << b.x[0]->y << endl;
    a.x[0]->y = 6;
    cout << b.x[0]->y << endl;

    return 0;
}

2.触发条件

  • 对象作为实参传递给非引用类型的形参(这也是拷贝构造函数形参为&的原因,如果不是引用,则会调用拷贝构造函数,这种行为是为定义的)
  • 从返回类型为非引用类型的函数返回一个对象
  • {}初始化列表——数组,聚合类(定义如下)
    • 所有成员都是public
    • 没有定义构造函数
    • 没有类内初始值
    • 没有基类,也没有虚函数
  • 容器调用insert和push,进行拷贝初始化

3.注意

  • 编译器为了优化,也会绕过拷贝构造函数,但拷贝构造必须存在且可访问

二、析构函数

1.特点

  • 没有返回值,不接受参数,因此不能被重载,只有唯一一个析构函数
  • 成员按初始化顺序的逆序销毁
  • 内置类型没有析构函数

2.触发条件

  • 变量离开作用域
  • 对象被销毁,成员被销毁
  • 容器被销毁,其元素也被销毁
  • delete销毁
  • 对于临时变量,当创建它的完整表达式结束时被销毁

3.注意

析构函数自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的


三、对象移动(参考知乎大佬

1.适用对象

IO类和unique_ptr类可以移动但不能拷贝

2.右值引用&&

  • 只能绑定到临时对象,不能绑定到一个变量上
  • 所引用的对象将要被销毁
  • 该对象没有其他用户
  • 具体到函数:
    emplcae_back 接收一个右值引用,调用其移动构造函数,将对象移动到容器中,而之前的push_back 是调用一次对象的拷贝构造函数, 容器中存储的是拷贝后的副本
  • 右值引用一定不能被左值所初始化,只能用右值初始化:
int x = 20;    // 左值
int&& rrx1 = x;   // 非法:右值引用无法被左值初始化
const int&& rrx2 = x;  // 非法:右值引用无法被左值初始化
  • const左值引用可以接收右值

3.移动语义(例子可以看知乎那篇文章

将内存的所有权从一个对象转移到另外一个对象,高效的移动用来替换效率低下的复制
通过内存所有权的接管来免去临时对象的复制操作

  • 编译器不会创建默认的移动构造函数和移动赋值运算符,所以4个函数(拷贝构造,赋值构造,移动构造,移动赋值)实现了一个,最好把剩余3个都实现了
  • move:左值转化为右值
vector<int> v1{1, 2, 3, 4};
vector<int> v2 = v1;             // 此时调用复制构造函数,v2是v1的副本
vector<int> v3 = std::move(v1);  // 此时调用移动构造函数,v3与v1交换:v1为空,v3为{1, 2, 3, 4}

典型应用swap

//C++11以前,拷贝构造和赋值
template <typename T>
void swap(T& a, T& b)
{
    T tmp{a};  // 调用复制构造函数
    a = b;     // 复制赋值运算符
    b = tmp;     // 复制赋值运算符
}

//C++11,移动构造和移动赋值
template <typename T>
void swap(T& a, T& b)
{
    T temp{std::move(a)};   // 调用移动构造函数
    a = std::move(b);       // 调用移动赋值运算符
    b = std::move(tmp);     // 调用移动赋值运算符
}

简化版move,详解见知乎文章

template <typename T>
typename remove_reference<T>::type&& move(T&& param)
{
    using ReturnType = typename remove_reference<T>::type&&;

    return static_cast<ReturnType>(param);
}
  • 注意:出现3个&符号的解释,我们知道C++不允许引用的引用,那么其实编译器这里进行是引用折叠(reference collapsing,大致就是后面的引用消掉)

4.完美转发(还不是很懂,留坑!)

定义一个函数模板,该函数模板可以接收任意类型参数,然后将参数转发给其它目标函数,且保证目标函数接受的参数其类型与传递给模板函数的类型相同


四、杂例

1.三五法则

  • 需要析构函数的类也需要拷贝和赋值操作
  • 需要拷贝操作的类也需要赋值操作,反之亦然

2.阻止拷贝(采用默认合成的拷贝)

  • =delete
  • 析构函数不能是删除的成员

运算符重载


一、基本概念

  • , & || &&不应该被重载
    :: .* . ?:不能被重载
  • 对于运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数
  • 成员函数还是非成员函数
    • = [] () ->必须是成员
    • 复合运算符(+=)一般也是成员,并非必须
    • 改变状态的运算符或者与给定类型密切相关的运算符,如++ – &,通常是成员
    • 具有对称性的运算符通常是普通的非成员函数

二、递增和递减运算符

  • 区分前置后置运算符:后置版本接受一个额外的int类型形参(后来者排队,所以总共有两个参数,前置只有一个)
  • 后置运算符应该返回对象原值返回的形式是一个值而非引用,与内置版本保持一致

三、函数调用运算符

  • lambda是函数对象:编译器将lambda表达式翻译成一个未命名类的未命名对象,在lambda表达式产生的类中含有一个重载的函数调用运算符(),lambda产生的类当中的函数调用运算符是一个const成员函数
  • 可调用对象与function:定义函数表,使用function存储,function是一个模板,function<int(int,int)>,表示接受两个int,返回一个int的可调用对象
  • 可调用对象的具体作用:使不同类型具有相同的调用形式

四、类型转换运算符

  • 没有显式返回类型,也没有形参,必须被定义成类的成员函数,且为const成员
  • 显式的类型转换将被隐式执行,即表达式被用作条件,则编译器会将显式的类型转换自动应用于它
    • if,while,do及语句的条件部分
    • for语句头的条件表达式
    • ! || && 的运算对象
    • ? : 的条件表达式

面向程序设计


一、虚函数

1.细节

基类中是虚函数,派生类中也是默认为虚函数

2.函数表剖析

#include <iostream>

using namespace std;

class Base1
{

public:

    virtual void f()
    { cout << "Base1::f" << endl; }

    virtual void g()
    { cout << "Base1::g" << endl; }

    virtual void h()
    { cout << "Base1::h" << endl; }

};

class Base2
{

public:

    virtual void f()
    { cout << "Base2::f" << endl; }

    virtual void g()
    { cout << "Base2::g" << endl; }

    virtual void h()
    { cout << "Base2::h" << endl; }

};

class Base3
{

public:

    virtual void f()
    { cout << "Base3::f" << endl; }

    virtual void g()
    { cout << "Base3::g" << endl; }

    virtual void h()
    { cout << "Base3::h" << endl; }

};

class Derive:
        public Base1, public Base2, public Base3
{

public:

    virtual void f()
    { cout << "Derive::f" << endl; }

    virtual void g1()
    { cout << "Derive::g1" << endl; }

};

typedef void(*Fun)(void);

int main()
{

    Fun pFun = nullptr;
    Derive d;
    long **pVtab = (long **) &d;

    //Base1's vtable

    //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+0);

    pFun = (Fun) pVtab[0][0];

    pFun();

    //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+1);

    pFun = (Fun) pVtab[0][1];

    pFun();

    //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+2);

    pFun = (Fun) pVtab[0][2];

    pFun();

    //Derive's vtable

    //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+3);

    pFun = (Fun) pVtab[0][3];

    pFun();

    //The tail of the vtable

    pFun = (Fun) pVtab[0][4];

    cout << pFun << endl;

    //Base2's vtable

    //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);

    pFun = (Fun) pVtab[1][0];

    pFun();

    //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);

    pFun = (Fun) pVtab[1][1];

    pFun();

    pFun = (Fun) pVtab[1][2];

    pFun();

    //The tail of the vtable

    pFun = (Fun) pVtab[1][3];

    cout << pFun << endl;

    //Base3's vtable

    //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);

    pFun = (Fun) pVtab[2][0];

    pFun();

    //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);

    pFun = (Fun) pVtab[2][1];

    pFun();

    pFun = (Fun) pVtab[2][2];

    pFun();

    //The tail of the vtable

    pFun = (Fun) pVtab[2][3];

    cout << pFun << endl;

    return 0;

}


二、类型转换与继承

  • 不存在基类向派生类的隐式类型转换,但有显式的(后续会讲)
  • 继承方式决定了在派生类中基类成员的访问权限(公有继承,保护继承,私有继承)

三、虚继承

  • 类通过虚继承指出它希望共享虚基类的状态
  • 主要解决菱形继承的资源浪费问题,D继承了两份相同的A,把A设置成虚基类可以解决这个问题,D中就只会有一份A的成员变量和方法
    在这里插入图片描述

模板与泛式编程


(这个有打算专门写一片博客,主要是太菜了,没怎么学明白,后续打算补一下这块的知识…)


标准库特殊设施


一、tuple

  • 任意数量成员,每个成员类型可以不同
  • tuple<T1,T2,T3,…,Tn> t (v1,v2,…,vn)

特殊工具与技术


一、RTTI(运行时类型识别)

  • typeid——abi::__cxa_demangle(typeid(a).name(), 0, 0, 0)使typeid获取类的完整声明
#include <iostream>
#include <cxxabi.h>

using namespace std;

class AB
{

};

int main()
{

    AB a;
    cout << typeid(a).name() << endl;//2AB,前面的2代表类的字符串长度为2
    cout << abi::__cxa_demangle(typeid(a).name(), 0, 0, 0) << endl;//AB,这一串可以只显示类名
    return 0;

}

  • dynamic_cast(将基类指针转化成派生类指针)
  • 提供了运行时确定对象类型的方法(区分于auto和decltype编译时确定对象类型)

二、枚举类型和共用体

1.枚举类型

enum

2.共用体

union变量所占用的内存长度等于最长的成员的内存长度(可用这个性质判断大小端)转自这位大佬

#include <stdio.h>

typedef union SmallBig
{

    char buf[4];
    long num;

} smallbig;

int main()
{

    smallbig sb;

    sb.num = 0x12345678;

    int i = 0;

    for (i = 0; i < 4; i++)
        printf("sb.buf[%d]=%x \n", i, sb.buf[i]);

    return 0;

}

/*
小端结果
sb.buf[0]=78 
sb.buf[1]=56 
sb.buf[2]=34 
sb.buf[3]=12 
*/

三、不可移植的特性

1.位域

规定变量占几个二进制位;int x:27——x占27位
充分利用内存空间

2.extern “C”

大佬解析


总结


完结撒花!接下来是暑假的一个专题总结,关于23种设计模式的总结,当然了也是像这样的简陋版本,争取在开学之前完成吧!最后附赠一张我C++总结的思维导图,内容和这三篇博客相差无几。思维导图

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值