绝赞春招拯救计划 -- C++篇

huihut/interview: 📚 C/C++ 技术面试基础知识总结,包括语言、程序库、数据结构、算法、系统、网络、链接装载库等知识及面试经验、招聘、内推等信息。This repository is a summary of the basic knowledge of recruiting job seekers and beginners in the direction of C/C++ technology, including language, program library, data structure, algorithm, system, network, link loading library, interview experience, recruitment, recommendation, etc. (github.com)icon-default.png?t=N7T8https://github.com/huihut/interview?tab=readme-ov-file#data-structure不太熟这个Union 联合是啥

union 联合

联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:

  • 默认访问控制符为 public
  • 可以含有构造函数、析构函数
  • 不能含有引用类型的成员
  • 不能继承自其他类,不能作为基类
  • 不能含有虚函数
  • 匿名 union 在定义所在作用域可直接访问 union 成员
  • 匿名 union 不能包含 protected 成员或 private 成员
  • 全局匿名联合必须是静态(static)的

感觉就是一个内存,可以用来作为好几种数据成员,只是只够放一个,具有多样性

union 使用

#include<iostream>

union UnionTest {
    UnionTest() : i(10) {};
    int i;
    double d;
};

static union {
    int i;
    double d;
};

int main() {
    UnionTest u;

    union {
        int i;
        double d;
    };

    std::cout << u.i << std::endl;  // 输出 UnionTest 联合的 10

    ::i = 20;
    std::cout << ::i << std::endl;  // 输出全局静态匿名联合的 20

    i = 30;
    std::cout << i << std::endl;    // 输出局部匿名联合的 30

    return 0;
}

explicit 使用

struct A
{
	A(int) { }
	operator bool() const { return true; }
};

struct B
{
	explicit B(int) {}
	explicit operator bool() const { return true; }
};

void doA(A a) {}

void doB(B b) {}

int main()
{
	A a1(1);		// OK:直接初始化
	A a2 = 1;		// OK:复制初始化
	A a3{ 1 };		// OK:直接列表初始化
	A a4 = { 1 };		// OK:复制列表初始化
	A a5 = (A)1;		// OK:允许 static_cast 的显式转换 
	doA(1);			// OK:允许从 int 到 A 的隐式转换
	if (a1);		// OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
	bool a6(a1);		// OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
	bool a7 = a1;		// OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
	bool a8 = static_cast<bool>(a1);  // OK :static_cast 进行直接初始化

	B b1(1);		// OK:直接初始化
	B b2 = 1;		// 错误:被 explicit 修饰构造函数的对象不可以复制初始化
	B b3{ 1 };		// OK:直接列表初始化
	B b4 = { 1 };		// 错误:被 explicit 修饰构造函数的对象不可以复制列表初始化
	B b5 = (B)1;		// OK:允许 static_cast 的显式转换
	doB(1);			// 错误:被 explicit 修饰构造函数的对象不可以从 int 到 B 的隐式转换
	if (b1);		// OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
	bool b6(b1);		// OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
	bool b7 = b1;		// 错误:被 explicit 修饰转换函数 B::operator bool() 的对象不可以隐式转换
	bool b8 = static_cast<bool>(b1);  // OK:static_cast 进行直接初始化

	return 0;
}
  • explicit 修饰构造函数时,可以防止隐式转换和复制初始化
  • explicit 修饰转换函数时,可以防止隐式转换,但 按语境转换 除外

按语境转换:

隐式转换 - cppreference.com

下列语境中,期待类型 bool,且如果声明 bool t(e); 良构就会进行隐式转换(即考虑如 explicit T::operator bool() const; 这样的隐式转换函数)。称这种表达式 e 按语境转换到 bool

  • if、while、for 的控制表达式;
  • 内建逻辑运算符 !&& 和 || 的操作数;
  • 条件运算符 ?: 的首个操作数;
  • static_assert 声明中的谓词;
  • noexcept 说明符中的表达式;
(C++20 起)

enum 限定作用域与不限定作用域

还是头一次听说这玩意儿还会限定作用域

C++两种枚举类型-限定作用域的枚举类型 和 不限定作用域的枚举型别_c 枚举作用域-CSDN博客

省流:

限定作用域的枚举类型

enum class open_modes { input, output, append };

不限定作用域的枚举类型

enum color { red, yellow, green };

enum { floatPrec = 6, doublePrec = 10 };

限定作用域的枚举类型通过 enum class 声明,不限定作用域的枚举类型通过enum声明

不限定作用域的:

enum Weekday{SUN,MON,TUE,SAT};  不能直接写赋值表达式

enum Weekday{SUN=7,MON=1,TUE,SAT};   可以在声明的时候指定

#include<iostream>
using namespace std;
enum GameResult{WIN,LOSE,TIE,CANCEL};

int main(){
	GameResult result;
	enum GameResult omit=CANCEL;
	// 上述两种方式都是可以的。带或者不带上enum关键字都可以。
	for(int count=WIN;count<=CANCEL;count++){
		// 这里是不限定作用域的枚举类型。所以可知直接比较(隐式类型转换),
		// 不需要显式类型转换
		result = static_cast<GameResult>(count);
		//不能直接用一个整数给枚举值赋值,需要进行强制类型转化。
		if (result==omit){
			cout<<"The game was cancelled"<<endl;
		}
		else{
			cout<<"The game was played ";
			if(result==WIN){
				cout<<"and we won!";
			}
			if(result==LOSE){
				cout<<"and we lost...";
			}
			cout<<endl;
		}
	}
	return 0;
}
1、不限定作用域的枚举型别可能导致枚举量泄漏到所在的作用域空间
namespace TestSpace {
enum Color {
  red = 0,
  green,
  blue,
};
auto red = true;  // 错误
}; // namespace TestSpace

在同一个作用域中,定义了不限定作用域的枚举类Color,然后定义了red变量。由于没限定作用域,所以外部也可以使用red。auto red重新定义了red所以报错。

对一个变量,只能声明一次,多次声名,就算声明类型相同,也是错误的。 而函数中,可以直接在 “函数原型” 中声明。

而限定作用域的枚举型别
namespace TestSpace {
enum class Color {
  red = 0,
  green,
  blue,
};
auto red = true; // 没问题
}; // namespace TestSpace

decltype

c++ decltype: 类型推导规则及应用 - 知乎 (zhihu.com)

获知表达式的类型

  • auto,用于通过一个表达式在编译时确定待定义的变量类型,auto 所修饰 的变量必须被初始化,编译器需要通过初始化来确定auto 所代表的类型,即必须要定义变 量。
  • 若仅希望得到类型,而不需要(或不能)定义变量的时候应该怎么办呢? C++11 新增了decltype 关键字,用来在编译时推导出一个表达式的类型。它的语法格式 如下:
decltype(exp)
其中,exp 表示一个表达式(expression)
int x = 0;
decltype(x) y = 1; // y -> int
decltype(x + y) z = 0; // z -> int
const int& i = x;
decltype(i) j = y; // j -> const int &
const decltype(z) * p = &z; // *p -> const int, p -> const int *
decltype(z) * pi = &z; // *pi -> int , pi -> int *
decltype(pi)* pp = &pi; // *pp -> int * , pp -> int * *

decltype 的推导规则

推导规则 1: exp 是标识符、类访问表达式,decltype(exp) 和 exp 的类型一致。

推导规则 2: exp 是函数调用,decltype(exp) 和返回值的类型一致。

推导规则 3: 其他情况,若 exp 是一个左值,则 decltype(exp) 是 exp 类型的左值引 用,否则和exp 类型一致。

函数调用

如果表达式是一个函数调用(不符合推导规则1),结果会如 何呢? 请看代码清单1-4 所示的示例。

int& func_int_r(void); // 左值(lvalue,可简单理解为可寻址值)
int&& func_int_rr(void); // x 值(xvalue,右值引用本身是一个xvalue)
int func_int(void); // 纯右值(prvalue,将在后面的章节中讲解)
const int& func_cint_r(void); // 左值
const int&& func_cint_rr(void); // x 值
const int func_cint(void); // 纯右值
const Foo func_cfoo(void); // 纯右值
// 下面是测试语句
int x = 0;
decltype(func_int_r()) a1 = x; // a1 -> int &
decltype(func_int_rr()) b1 = 0; // b1 -> int &&
decltype(func_int()) c1 = 0; // c1 -> int
decltype(func_cint_r()) a2 = x; // a2 -> const int &
decltype(func_cint_rr()) b2 = 0; // b2 -> const int &&
decltype(func_cint()) c2 = 0; // c2 -> int
decltype(func_cfoo()) ff = Foo(); // ff -> const Foo
  1. 按照推导规则2,decltype 的结果和函数的返回值类型保持一致。
  2. 需要注意的是,c2 是int 而不是const int。这是因为函数返回的int 是一个纯右值 (prvalue)。对于纯右值而言,只有类类型可以携带cv 限定符,此外则一般忽略掉cv 限定。

带括号的表达式和加法运算表达式

最后,来看看第三种情况:

struct Foo { int x; };
const Foo foo = Foo();
decltype(foo.x) a = 0; // a -> int
decltype((foo.x)) b = a; // b -> const int &
int n = 0, m = 0;
decltype(n + m) c = 0; // c -> int
decltype(n += m) d = c; // d -> int &
  1. a 和b 的结果:仅仅多加了一对括号,它们得到的类型却是不同的。
  2. a 的结果是很直接的,根据推导规则1,a 的类型就是foo.x 的定义类型。
  3. b 的结果并不适用于推导规则1 和2。根据foo.x 是一个左值,可知括号表达式也是一个 左值。因此可以按照推导规则3,知道decltype 的结果将是一个左值引用。 foo 的定义是const Foo,所以foo.x 是一个const int 类型左值,因此decltype 的推导结果 是const int &。 同样,n+m 返回一个右值,按照推导规则3,decltype 的结果为int。 最后,n+=m 返回一个左值,按照推导规则3,decltype 的结果为int &。

decltype 使用

// 尾置返回允许我们在参数列表之后声明返回类型
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
    // 处理序列
    return *beg;    // 返回序列中一个元素的引用
}
// 为了使用模板参数成员,必须用 typename
template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
    // 处理序列
    return *beg;    // 返回序列中一个元素的拷贝
}

虚函数相关

  • 静态函数(static)不能是虚函数
  • 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)

虚析构函数

虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。

虚析构函数使用

class Shape
{
public:
    Shape();                    // 构造函数不能是虚函数
    virtual double calcArea();
    virtual ~Shape();           // 虚析构函数
};
class Circle : public Shape     // 圆形类
{
public:
    virtual double calcArea();
    ...
};
int main()
{
    Shape * shape1 = new Circle(4.0);
    shape1->calcArea();    
    delete shape1;  // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。
    shape1 = NULL;
    return 0;
}

带纯虚函数的类叫虚基类,这种基类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。这样的类也叫抽象类。抽象类和大家口头常说的虚基类还是有区别的,在C#中用abstract定义抽象类,而在C++中有抽象类的概念,但是没有这个关键字。抽象类被继承后,子类可以继续是抽象类,也可以是普通类,而虚基类,是含有纯虚函数的类,它如果被继承,那么子类就必须实现虚基类里面的所有纯虚函数,其子类不能是抽象类。

虚继承

虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。

底层实现原理与编译器相关,一般通过虚基类指针虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

虚继承、虚函数

  • 相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
  • 不同之处:
    • 虚继承
      • 虚基类依旧存在继承类中,只占用存储空间
      • 虚基类表存储的是虚基类相对直接继承类的偏移
    • 虚函数
      • 虚函数不占用存储空间
      • 虚函数表存储的是虚函数地址

什么叫做相对直接继承类的偏移?

彻底弄懂C++虚拟继承-CSDN博客

  当使用虚拟继承时,原本的父类成员 会被替换为 一个虚基表指针,这个指针指向一张虚基表,虚基表里存放 虚基类与虚基表指针的偏移量

一 虚基类的位置

      (一)虚基类储存在最后继承它的派生类(后文简称最后派生类)的末尾

1.teacher继承了虚基类perso

 2.worker为teacher的派生类
3.worker继承虚基类person

 4.worker为虚基类person的最后派生类

teacher的虚基表里储存的偏移量为12

(二)一个类可以是多个虚基类的最后派生类,这些虚基类按照继承顺序依次储存这个类的末尾


class person {
public:
	int _person_age=18;
};
class teacher :virtual public person{
public:
	int teacher_id=17;
};
class worker : virtual public  teacher{
	int worker_id =16;

虚基类只被继承只有一份,派生类通过虚基表指针获取到偏移量,找到虚基类的位置。

二 虚基类的声明 初始化 及其他
(一).虚基类在指定虚继承方式(使用virtual)时声明。

(二).根据:

        1. 一个类调用构造函数的顺序:虚基类->直接基类->类

        2. 虚基类只初始化一次

得处结论

        i.由于虚基类最先初始化,所以只能由最后派生类调用虚基类的构造函数初始化虚基类部分

        ii.最后派生类 还负责调用直接基类的构造函数初始化直接基类部分,并且调用直接基类时不会再次调用虚基类的构造函数。

(三)类的析构顺序:类->直接基类->虚基类(与构造相反)

(四)类的赋值,拷贝顺序:虚基类->直接基类->类(与构造一致)

类模板,成员模板,虚函数

  • 类模板中可以使用虚函数
  • 一个类(无论是普通类还是类模板)的成员模板(本身是模板的成员函数)不能是虚函数

强制类型转换运算符

经常看见,但是不会

static_cast
  • 用于非多态类型的转换
  • 不执行运行时类型检查(转换安全性不如 dynamic_cast)
  • 通常用于转换数值数据类型(如 float -> int)
  • 可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类可能有不在父类的字段或方法)

向上转换是一种隐式转换。

dynamic_cast
  • 用于多态类型的转换
  • 执行行运行时类型检查
  • 只适用于指针或引用
  • 对不明确的指针的转换将失败(返回 nullptr),但不引发异常
  • 可以在整个类层次结构中移动指针,包括向上转换、向下转换
const_cast
  • 用于删除 const、volatile 和 __unaligned 特性(如将 const int 类型转换为 int 类型 )
reinterpret_cast
  • 用于位的简单重新解释
  • 滥用 reinterpret_cast 运算符可能很容易带来风险。 除非所需转换本身是低级别的,否则应使用其他强制转换运算符之一。
  • 允许将任何指针转换为任何其他指针类型(如 char* 到 int* 或 One_class* 到 Unrelated_class* 之类的转换,但其本身并不安全)
  • 也允许将任何整数类型转换为任何指针类型以及反向转换。
  • reinterpret_cast 运算符不能丢掉 const、volatile 或 __unaligned 特性。
  • reinterpret_cast 的一个实际用途是在哈希函数中,即,通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引。
bad_cast
  • 由于强制转换为引用类型失败,dynamic_cast 运算符引发 bad_cast 异常。

bad_cast 使用

try {  
    Circle& ref_circle = dynamic_cast<Circle&>(ref_shape);   
}  
catch (bad_cast b) {  
    cout << "Caught: " << b.what();  
} 

看不懂哥们 

C++强制类型转换运算符(static_cast、reinterpret_cast、const_cast和dynamic_cast) (biancheng.net)

将类型名作为强制类型转换运算符的做法是C语言的老式做法,C++ 为保持兼容而予以保留。

C++ 引入新的强制类型转换机制,主要是为了克服C语言强制类型转换的以下三个缺点

1) 没有从形式上体现转换功能和风险的不同。

例如,将 int 强制转换成 double 是没有风险的,而将常量指针转换成非常量指针,将基类指针转换成派生类指针都是高风险的,而且后两者带来的风险不同(即可能引发不同种类的错误),C语言的强制类型转换形式对这些不同并不加以区分。

2) 将多态基类指针转换成派生类指针时不检查安全性,即无法判断转换后的指针是否确实指向一个派生类对象。

3) 难以在程序中寻找到底什么地方进行了强制类型转换。

C++ 强制类型转换运算符的用法如下:

强制类型转换运算符 <要转换到的类型> (待转换的表达式)

例如:

double d = static_cast <double> (3*5);  //将 3*5 的值转换成实数

static_cast 用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型之间的互相转换。另外,如果对象所属的类重载了强制类型转换运算符 T(如 T 是 int、int* 或其他类型名),则 static_cast 也能用来进行对象到 T 类型的转换。

static_cast 不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。因为这些属于风险比较高的转换。

static_cast 用法示例如下:

#include <iostream>
using namespace std;
class A
{
public:
    operator int() { return 1; }
    operator char*() { return NULL; }
};
int main()
{
    A a;
    int n;
    char* p = "New Dragon Inn";
    n = static_cast <int> (3.14);  // n 的值变为 3
    n = static_cast <int> (a);  //调用 a.operator int,n 的值变为 1
    p = static_cast <char*> (a);  //调用 a.operator char*,p 的值变为 NULL
    n = static_cast <int> (p);  //编译错误,static_cast不能将指针转换成整型
    p = static_cast <char*> (n);  //编译错误,static_cast 不能将整型转换成指针
    return 0;
}

reinterpret_cast

reinterpret_cast 用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换。转换时,执行的是逐个比特复制的操作。

这种转换提供了很强的灵活性,但转换的安全性只能由程序员的细心来保证了。例如,程序员执意要把一个 int* 指针、函数指针或其他类型的指针转换成 string* 类型的指针也是可以的,至于以后用转换后的指针调用 string 类的成员函数引发错误,程序员也只能自行承担查找错误的烦琐工作:

reinterpret_cast 用法示例如下:

#include <iostream>
using namespace std;
class A
{
public:
    int i;
    int j;
    A(int n):i(n),j(n) { }
};
int main()
{
    A a(100);
    int &r = reinterpret_cast<int&>(a); //强行让 r 引用 a
    r = 200;  //把 a.i 变成了 200
    cout << a.i << "," << a.j << endl;  // 输出 200,100
    int n = 300;
    A *pa = reinterpret_cast<A*> ( & n); //强行让 pa 指向 n
    pa->i = 400;  // n 变成 400
    pa->j = 500;  //此条语句不安全,很可能导致程序崩溃
    cout << n << endl;  // 输出 400
    long long la = 0x12345678abcdLL;
    pa = reinterpret_cast<A*>(la); //la太长,只取低32位0x5678abcd拷贝给pa
    unsigned int u = reinterpret_cast<unsigned int>(pa);//pa逐个比特拷贝到u
    cout << hex << u << endl;  //输出 5678abcd
    typedef void (* PF1) (int);
    typedef int (* PF2) (int,char *);
    PF1 pf1;  PF2 pf2;
    pf2 = reinterpret_cast<PF2>(pf1); //两个不同类型的函数指针之间可以互相转换
}

第 19 行的代码不安全,因为在编译器看来,pa->j 的存放位置就是 n 后面的 4 个字节。 本条语句会向这 4 个字节中写入 500。但这 4 个字节不知道是用来存放什么的,贸然向其中写入可能会导致程序错误甚至崩溃。

const_cast

const_cast 运算符仅用于进行去除 const 属性的转换,它也是四个强制类型转换运算符中唯一能够去除 const 属性的运算符。

将 const 引用转换为同类型的非 const 引用,将 const 指针转换为同类型的非 const 指针时可以使用 const_cast 运算符。例如:

const string s = "Inception";
string& p = const_cast <string&> (s);
string* ps = const_cast <string*> (&s);  // &s 的类型是 const string*

dynamic_cast

用 reinterpret_cast 可以将多态基类(包含虚函数的基类)的指针强制转换为派生类的指针,但是这种转换不检查安全性,即不检查转换后的指针是否确实指向一个派生类对象。dynamic_cast专门用于将多态基类的指针或引用强制转换为派生类的指针或引用,而且能够检查转换的安全性。对于不安全的指针转换,转换结果返回 NULL 指针。

dynamic_cast 是通过“运行时类型检查”来保证安全性的。dynamic_cast 不能用于将非多态基类的指针或引用强制转换为派生类的指针或引用——这种转换没法保证安全性,只好用 reinterpret_cast 来完成。

dynamic_cast 示例程序如下:

#include <iostream>
#include <string>
using namespace std;
class Base
{  //有虚函数,因此是多态基类
public:
    virtual ~Base() {}
};
class Derived : public Base { };
int main()
{
    Base b;
    Derived d;
    Derived* pd;
    pd = reinterpret_cast <Derived*> (&b);
    if (pd == NULL)
        //此处pd不会为 NULL。reinterpret_cast不检查安全性,总是进行转换
        cout << "unsafe reinterpret_cast" << endl; //不会执行
    pd = dynamic_cast <Derived*> (&b);
    if (pd == NULL)  //结果会是NULL,因为 &b 不指向派生类对象,此转换不安全
        cout << "unsafe dynamic_cast1" << endl;  //会执行
    pd = dynamic_cast <Derived*> (&d);  //安全的转换
    if (pd == NULL)  //此处 pd 不会为 NULL
        cout << "unsafe dynamic_cast2" << endl;  //不会执行
    return 0;
}

这下看懂了

运行时类型识别(RTTI)

不熟,就只是听说过

  • typeid 运算符,用于返回表达式的类型。
  • dynamic_cast 运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。

为 RTTI 提供的第二个运算符是 typeid 运算符(typeid operator),它允许程序向表达式提问:你的对象是什么类型?

typeid(e)
  • e 可以是任意表达式或类类型;
  • typeid 返回值是对一个常量对象的引用,该对象的类型是标准库类型 type_info 或者 type_info 的公有派生类型;
  • e 如果是引用,typeid 会返回该引用所引用的对象类型;
  • typeid 作用在数组上时,返回的是数组类型,而不是指针类型;
  • 运算对象不是类类型,或者不包含虚函数的类,指示的是静态类型;
  • 如果是定义了一个虚函数的类型,typeid 的结果运行时才会求得;
// rtti/typeid.cpp
#include <iostream>
using namespace std;

class Base {
public:
    Base(int _i) : i(_i) {}
    virtual void print() {
        cout << " i = " << i << endl;
    }
private:
    int i;
};

class Derive : public Base {
public:
    Derive(int i) : Base(i) {}
};

int main() {
    Derive *dp = new Derive(10);
    Base *bp = dp; // 两个指针都指向Derive对象

    if (typeid(*bp) == typeid(*dp)) {
        cout << "bp和dp指向同一个类型的对象" << endl;
    }

    if (typeid(*bp) == typeid(Derive)) {
        cout << "bp指向的是Derive对象" << endl;
    }
    return 0;
}
/*
$ g++ rtti2.cpp && ./a.out
bp和dp指向同一个类型的对象
bp指向的是Derive对象
*/

注:

  • typeid 应该作用于对象,而不是指针本身,因此需要使用 *bp
  • 只有当类型有虚函数的时候,编译器才会在运行时对表达式进行求值;
  • 如果类型没有虚函数,typeid 返回的是静态类型,编译器无须求值也知道表达式的静态类型;
  • 如果指针 p 指向的类型没有虚函数,则计算 typeid(*p) 的时候 *p 可以不是一个有效的对象;
  • 如果指针 p 指向的类型有虚函数, *p 会在运行时求值,如果 p 是一个空指针,会抛出 bad_typeid 的异常;

使用 RTTI

现有一个基类和一个派生类,我们需要为其实现相等运算符,一种考虑的方法是定义一套虚函数,然后各自在判断是否相等。

这样的问题是因为虚函数中基类和派生类必须有相同的形参,如果我们想定义一个虚函数 equal,则该函数的形参必须是基类的引用。此时,equal 函数将只能使用基类的成员,而不能比较派生类独有的成员。

下面是解决方法,可以利用 typeid 先判断类型是否相同,然后再调用各自版本的 equal 函数:

// rtti/rtti_example.cpp
#include <iostream>
using namespace std;

class Base {
    friend bool operator==(const Base&, const Base&);
public:
    Base(int _i, int _j) : i(_i), j(_j) {}
protected:
    virtual bool equal(const Base&) const;
    int i;
    int j;
};

class Derive : public Base {
public:
    Derive(int _i, int _j, int _k) : Base(_i, _j), k(_k) {}
protected:
    bool equal(const Base&) const;
private:
    int k;
};

bool operator==(const Base &lhs, const Base &rhs) {
    // 如果typeid不相等返回false;否则调用equal()
    return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}

bool Derive::equal(const Base &rhs) const {
    cout << "Derive::equal()" << endl;
    // 我们清楚这两个类型是相等的,所以转换过程不会抛出异常
    auto r = dynamic_cast<const Derive&>(rhs);
    // 执行比较两个Derive对象的操作并返回
    return this->i == r.i && this->j == r.j && this->k == r.k;
}

bool Base::equal(const Base &rhs) const {
    cout << "Base::equal()" << endl;
    // 执行比较Base对象的操作
    return this->i == rhs.i && this->j == rhs.j;
}

int main() {
    // 第一种情况:两个基类指针进行判断
    Base *bp1 = new Base(10, 20);
    Base *bp2 = new Base(10, 20);
    if (*bp1 == *bp2) {
        cout << "bp1和bp2相等" << endl;
    }
    // 第二种情况:一个基类指针和一个派生类指针,都指向派生类对象
    Base *bp = new Derive(10, 20, 30);
    Derive *dp = new Derive(10, 20, 30);
    if (*bp == *dp) {
        cout << "bp和dp指向的对象相同" << endl;
    }
    // 第三种情况:两个指针分别指向基类对象和派生类对象,他两不能直接判断
    Base *bp3 = new Base(10, 20);
    Base *bp4 = new Derive(10, 20, 30);
    if (*bp3 == *bp4) {
        cout << "bp3和bp4指向的对象相同" << endl;
    } else {
        cout << "bp3和bp4指向的对象不相同" << endl;
    }
    return 0;
}
/*
$ g++ rtti_example.cpp && ./a.out
Base::equal()
bp1和bp2相等
Derive::equal()
bp和dp指向的对象相同
bp3和bp4指向的对象不相同
*/

 Lambda 

C++ Lambda表达式的完整介绍 - 知乎 (zhihu.com)

常见的使用形式: 

auto plus = [] (int v1, int v2) -> int { return v1 + v2; }
int sum = plus(1, 2);

std::sort(vec.begin(), vec.end(),
        [] (const Item& v1, const Item& v2) { return v1.a < v2.a; });

[=] 按值的方式捕获所有变量

[&] 按引用的方式捕获所有变量

[=, &a] 除了变量a之外,按值的方式捕获所有局部变量,变量a使用引用的方式来捕获。这里可以按引用捕获多个,例如 [=, &a, &b,&c]。这里注意,如果前面加了=,后面加的具体的参数必须以引用的方式来捕获,否则会报错。

[&, a] 除了变量a之外,按引用的方式捕获所有局部变量,变量a使用值的方式来捕获。这里后面的参数也可以多个,例如 [&, a, b, c]。这里注意,如果前面加了&,后面加的具体的参数必须以值的方式来捕获。

[a, &b] 以值的方式捕获a,引用的方式捕获b,也可以捕获多个。

[this] 在成员函数中,也可以直接捕获this指针,其实在成员函数中,[=]和[&]也会捕获this指针。

按值捕获参数的话生成的函数是const ,无法修改捕获的值,如果想要在函数内修改捕获的值,需要加上关键字 mutable。向下面这样的形式。

int x = 1; int y = 2;
auto plus = [=] (int a, int b) mutable -> int { x++; return x + y + a + b; };
int c = plus(1, 2);

 lambda编译器实现原理

原文链接:C++进阶(八) :Lambda 表达式及底层实现原理【详解】_lambda表达式底层c-CSDN博客

  1. 创建 lambda匿名类,实现构造函数,使用 lambda 表达式的函数体重载 operator()(所以 lambda 表达式 也叫匿名函数对象)
  2. 创建 lambda 对象
  3. 通过对象调用 operator()
class lambda_xxxx
{
private:
    int a;
    int b;
public:
    lambda_xxxx(int _a, int _b) :a(_a), b(_b)
    {
    }
    bool operator()(int x, int y) throw()
    {
        return a + b > x + y;
    }
};
void LambdaDemo()
{
    int a = 1;
    int b = 2;
    lambda_xxxx lambda = lambda_xxxx(a, b);
    bool ret = lambda.operator()(3, 4);
}
  • lambda 表达式中的捕获列表,对应 lambda_xxxx 类的 private 成员
  • lambda 表达式中的形参列表,对应 lambda_xxxx 类成员函数 operator() 的形参列表
  • lambda 表达式中的 mutable,表明 lambda_xxxx 类成员函数 operator() 的是否具有常属性 const,即是否是 常成员函数
  • lambda 表达式中的返回类型,对应 lambda_xxxx 类成员函数 operator() 的返回类型
  • lambda 表达式中的函数体,对应 lambda_xxxx 类成员函数 operator() 的函数体

另外,lambda 表达 捕获列表的捕获方式,也影响 对应 lambda_xxxx 类的 private 成员 的类型

  1. 值捕获:private 成员的类型与捕获变量的类型一致
  2. 引用捕获:private 成员 的类型是捕获变量的引用类型

如果 lambda 表达式不捕获任何外部变量,且有有 lambda_xxxx 类 到 函数指针 的类型转换,会有额外的代码生成,例如:

typedef int(_stdcall *Func)(int);
int Test(Func func)
{
	return func(1);
}
void LambdaDemo()
{
	Test([](int i) {
		return i;
	});
}

Test 函数接受一个函数指针作为参数,并调用这个函数指针。实际调用 Test 时,传入的参数却是一个 Lambda 表达式,所以这里有一个类型的隐式转换:lambda_xxxx => 函数指针。

上面已经提到,Lambda 表达式就是一个 lambda_xxxx 类的匿名对象,与函数指针之间按理说不应该存在转换,但是上述代码却没有问题。

其问题关键在于,上述代码中,lambda 表达式没有捕获任何外部变量,即 lambda_xxxx 类没有任何成员变量,在 operator() 中也就不会用到任何成员变量,也就是说,operator() 虽然是个成员函数,它却不依赖 this 就可以调用。

因为不依赖 this,所以 一个 lambda_xxxx 类的匿名对象与函数指针之间就存在转换的可能。

大致过程如下:

  1. 在 lambda_xxxx 类中生成一个静态函数,静态函数的函数签名与 operator() 一致,在这个静态函数中,通过一个空指针去调用该类的 operator()
  2. 在 lambda_xxxx 重载与函数指针的类型转换操作符,在这个函数中,返回静态函数的地址。
typedef int(_stdcall *Func)(int);
 
class lambda_xxxx 
{
private:
	//没有捕获任何外部变量,所有没有成员
public:
        /*...省略其他代码...*/
	int operator()(int i)
	{
		return i;
	}
	static int _stdcall lambda_invoker_stdcall(int i)
	{
		return ((lambda_xxxx *)nullptr)->operator()(i);
	}
 
	operator Func() const
	{
		return &lambda_invoker_stdcall;
	}
};
 
int Test(Func func)
{
	return func(1);
}
void LambdaDemo()
{
	auto lambda = lambda_xxxx ();
	Func func = lambda.operator Func();
	Test(func);
}

什么叫做用空指针去调取方法啊,不懂

这个_stdcall lambda_invoker_stdcall 是什么?

override 关键字

C++-[override]关键字使用详解_c++ override-CSDN博客

C++11 中的 override 关键字,可以显式的在派生类中声明哪些成员函数需要被重写,如果没被重写,则编译器会报错。可以写来规范代码

在派生类中,重写 (override) 继承自基类成员函数的实现 (implementation) 时,要满足如下条件:

一虚:基类中,成员函数声明为虚拟的 (virtual)
二容:基类和派生类中,成员函数的返回类型和异常规格 (exception specification) 必须兼容
四同:基类和派生类中,成员函数名、形参类型、常量属性 (constness) 和 引用限定符 (reference qualifier) 必须完全相同

class Base
{
public:
	virtual void fun1() const;
	virtual void fun2(int x);
	virtual void fun3() &;
	void fun4() const;    // is not declared virtual in Base

};

class Derived : public Base
{
public:
	void fun1() const override; 
	void fun2(int x) override;  
	void fun3() & override;   
	void fun4() const;   
};

C++ 堆和自由存储区的区别?

https://www.cnblogs.com/QG-whz/p/5060894.html

从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。我们所需要记住的就是:

堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。

new和malloc的区别及底层实现原理

从内存区域,重载,自定义类型,分配成功,返回类型,参数等方面来具体分析一下 new 和 malloc 的区别。

内存区域

new操作符从自由存储区上为对象动态分配内存空间(“堆对象”或“在动态存储中建立”),而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。

而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。

内置类型
  1. new / delete 申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,
  2. new在申请空间失败时会抛异常, malloc会返回NULL。
自定义类型
  • new的原理
    • 调用operator new函数申请空间
    • 在申请的空间上执行构造函数,完成对象的构造
  • delete的原理
    • 在空间上执行析构函数,完成对象中资源的清理工作
    • 调用operator delete函数释放对象的空间

new T[N]的原理

  •  调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
  •  在申请的空间上执行N次构造函数

delete[]的原理

  •  在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
  •  调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
重载

new和delete可以被重载,malloc和free不可以

返回类型

malloc返回void*,需要强制转换类型。

new返回对象类型的指针,类型严格与对象匹配。在分配的时候,首先是调用了operator new/new[](operator new/new[]函数和new表达式是两个东西)operator new/new[]返回的是一个void*类型的指针,指向一块原始空间,之后new再调用对象的构造函数,最终,再将void*指针类型转换成对象的类型的指针返回。

分配失败返回值类型不同

new:bac_alloc异常

(我们也可以指定使用不抛出异常的new版本,如:int* p = new(nothorw) int;传递给operator new 一个nothorw对象,该对象定义在头文件。若分配失败,返回一个空指针)

malloc:NULL(即数值上为0)

try
{
    int *a = new int();
}
catch (bad_alloc)
{
    ...
}  //有效的检查

int* num = new int();
if(num == NULL)  //无效的检查
参数(是否需要制定内存大小)

new无须指定内存块大小,编译器自行计算,malloc需要。 比如动态分配一个数组:

int* p1 = (int*)malloc(sizeof(int)*length);
int* p2 = new int[length];

是否调用构造函数/析构函数

对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。因为对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

使用 new 操作符来分配对象内存会经历五个步骤:

  •     调用operator new 函数(对于数组是operator new[]),分配一块足够大的未命名的内存空间
  •     编译器调用对象相应的构造函数以构造对象并传入初值。
  •     对象构造完毕,返回一个指向该对象的指针
  •     调用对象的析构函数以销毁对象
  •     编译器调用operator delete函数(对于数组是operator delete[])释放内存空间

new 的底层实现

看原文:new和malloc的区别及底层实现原理_new和malloc的区别以及底层实现原理-CSDN博客

左值,左值引用,右值,右值引用

左值、左值引用、右值、右值引用 - 那一剑的風情 - 博客园 (cnblogs.com)

C++ std::move 

c++ 之 std::move 原理实现与用法总结_std move-CSDN博客

复习Effective C++  55条 More Effective C++ 

《Effective C++》学习笔记-CSDN博客

《More Effective C++》学习-CSDN博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值