CPP易错点记录

拷贝构造函数和赋值运算符

在默认情况下(用户没有定义,但是也没有显式的删除),编译器会自动的隐式生成一个拷贝构造函数和赋值运算符。但用户可以使用delete来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。
拷贝构造函数必须以引用的方式传递参数。这是因为,在值传递的方式传递给一个函数的时候,会调用拷贝构造函数生成函数的实参。如果拷贝构造函数的参数仍然是以值的方式,就会无限循环的调用下去,直到函数的栈溢出。
拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;赋值运算符是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Person
{
public:
Person() {}
Person(const Person& p) {
cout << "Copy Constructor" << endl;
}

Person& operator=(const Person& p) {
cout << "Assign" << endl;
return *this;
}

private:
int age;
string name;
};

void f(Person p)
{
return;
}

Person f1()
{
Person p;
return p;
}

int main()
{
Person p;
Person p1 = p; // 这是虽然使用了"=",但是实际上使用对象p来创建一个新的对象p1。也就是产生了新的对象,所以调用的是拷贝构造函数。
Person p2;
p2 = p; // 首先声明一个对象p2,然后使用赋值运算符"=",将p的值复制给p2,显然是调用赋值运算符,为一个已经存在的对象赋值 。
f(p2); // 以值传递的方式将对象p2传入函数f内,调用拷贝构造函数构建一个函数f可用的实参。
p2 = f1(); // 这条语句拷贝构造函数和赋值运算符都调用了。函数f1以值的方式返回一个Person对象,在返回时会调用拷贝构造函数创建一个临时对象tmp作为返回值;返回后调用赋值运算符将临时对象tmp赋值给p2
Person p3 = f1(); // 按照上一条的解释,应该是首先调用拷贝构造函数创建临时对象;然后再调用拷贝构造函数使用刚才创建的临时对象创建新的对象p3,也就是会调用两次拷贝构造函数。不过,编译器也没有那么傻,应该是直接调用拷贝构造函数使用返回值创建了对象p3。

getchar();
return 0;
}

C++ std::string写时复制与深浅拷贝,默认是浅拷贝,写时进行深拷贝!如果要强制使用深拷贝,使用地址c_str()进行构造。

1
2
3
4
5
6
7
8
std::string a("test");
std::string b = a;
printf("%p: %s\n", a.c_str(), a.c_str());
printf("%p: %s\n", b.c_str(), b.c_str());
std::cout << "---after---" << std::endl;
b[0] = 'T';
printf("%p: %s\n", a.c_str(), a.c_str());
printf("%p: %s\n", b.c_str(), b.c_str());

1
2
3
4
5
6
7
8
9
10
const int* c_PtrA  =  new int(10);
int * ptrB= new int(10);

c_PtrA = ptrB; // 没问题
ptrB = c_PtrA; // 编译报错 不能从 const int* 转换程 int*

// 下面这种情况不会报错,但可能造成程序崩溃
ptrB = (int*)c_PtrA;
// ....
*ptrB = 20; // 此处修改了b和a共同指向的地址的内容,程序崩溃
  1. const int * 表示,指向常量int 类型的指针,即指向的这块内存的内容,不可以修改。(但指针本身可以修改)。
  2. int * 表示,指向非常量int类型的指针,指针这块内存的内容可修改,指针本身可修改。
  3. const int 已经限制此地址内容,不可修改。这时,却让 int 指针指向这块地址,而使用 int * 指针,表示此地址内容可修改。那么到底可不可以改?
  4. 接上,因此逻辑冲突,编译器报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum COLOR {
RED,
BLUE,
BLACK,
};

int main()
{
int i = 8;
COLOR j = RED;
COLOR k = (COLOR)i; // 需要强制转换
std::cout << "i = " << i << std::endl;
std::cout << "j = " << j << std::endl;
std::cout << "k = " << k << std::endl;
return 0;
}

// i = 8
// j = 0
// k = 8 超过了BLACK

C++中的函数隐藏机制,子类重载了父类的函数,父类的函数就被隐藏了,除非加using或基类限定符。
我们假设,没有隐藏机制,子类可以继承父类的其他所用同名的函数,那么会出现什么问题?
单继承是没问题的,多继承呢,如果派生类继承自Base1, Base2。而两个基类都用好几个func函数,那么派生类该使用哪一个呢?
引入函数隐藏机制,就解决了这个问题,如果使用Base1,就using Base1::func; 使用Base2中的函数,就using Base2::func;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
public:
virtual void test(int a, int b)
{
std::cout << a << " " << b << std::endl;
}
};

class Derived : public Base {
public:
// using Base::test;
void test()
{
int a = 1;
int b = 4;
Base::test(a, b);
std::cout << "Derived" << std::endl;
}
};

malloc的时候发生异常,异常处理函数里面在malloc就会阻塞,如下程序,运行时按ctrl+c会复现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <pthread.h>
using namespace std;
void signal_handler(int signum);

#define NUM 100000

void* test( void* args )
{
while(1)
{
printf("try to malloc in test, thread id = %u\r\n", pthread_self());
void *p = malloc(NUM);
}

return NULL;
}

int main()
{
signal(SIGINT, signal_handler);

pthread_t thread1;
pthread_create(&thread1, NULL, test, NULL);

for(;;)
{
printf("malloc in main function, thread id = %u\r\n", pthread_self());
void *p = malloc(NUM);
}

pthread_join(thread1, NULL);

return 0;
}

void signal_handler(int signum)
{
printf("receive SIGTERM, malloc again, thread id = %u\r\n", pthread_self());
void *p = malloc(NUM);
}

memset to struct 引起的 core,因为,memset(&,0,sizeof()) 会把 struct 结构体内的 所有复位 为0,内含的 string 对象 被毁坏了,在析构时 string对象的析构调用问题,对应 struct 内含 对象 最好不要用 memset 这类函数。

在 C++ 的多继承中,每个基类都会有一个对应的虚函数表(vtable)来存储其虚函数的地址。当派生类继承了多个基类时,其内存结构中将会有多份虚函数表。


菱形继承会导致类中出现重复的数据和函数,增加代码的复杂性和内存占用。为了避免这个问题,C++提供了虚继承(virtual inheritance)机制,通过这个机制来解决多重继承时的菱形继承问题。虚继承可以保证共同的基类在最终的派生类中只存在一份共享实例,有效地避免了菱形继承问题。
虚继承的内存布局情况如下:

  1. 如果一个类不是虚基类,则按照普通的继承方式,将基类的数据成员和成员函数复制到派生类中。
  2. 如果一个类是虚基类,则在派生类中只保留一个指向虚基类的指针v_ptr,不直接复制虚基类的成员数据和函数。这个指针是在虚表中的,虚表指针指向了指向虚基类的指针。
    同名虚函数在两个或更多父类中出现时,需要在派生类中明确地重写虚函数并使用作用域解析运算符来指定要使用哪个父类中的实现。
1
2
3
4
5
6
class Derived: public A, public B {
public:
void virtualFunc() {
A::virtualFunc(); // This calls the virtualFunc() of class A
}
};

结构体内存对齐
内存中的任何数据的存储地址必须是其本身数据类型大小的整数倍,否则会发生内存地址对齐错误(alignment glitch),这也是内存对齐最基本的原则。
举个例子:如果一个 int 类型的变量从内存地址 X 开始存储,那么地址 X 的值必须是 4 的倍数,否则就是不对齐的情况。

结构体对齐原则:

  1. 结构体每个成员变量的起始地址相对于结构体首地址的偏移量必须为该成员大小的整数倍。
  2. 结构体类型的总大小必须是结构体所有非位域成员大小的整数倍。换言之,结构体的大小必须是其成员中所占空间最大的成员大小的整数倍。
  3. 优化原则:结构体内的基本变量类型(比如 int,char 等)可以不对齐,但结构体内自定义类型的变量一定要对齐。因为自定义类型会在内存中开辟一段新的存储空间,不对齐很容易导致开发中易错难调的问题。

如何限制对象只能建立在堆上或者栈上
把构造、析构函数设为 protected 属性,再用子类来动态创建,类对象就无法建立在栈上了。只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。

define 和 const 区别
对于 define 来说, 宏定义实际上是在预编译阶段进行处理,没有类型,也就没有类型检查,仅仅做的是遇到宏定义进行字符串的展开,遇到多少次就展开多少次,而且这个简单的展开过程中,很容易出现边界效应,达不到预期的效果。因为 define 宏定义仅仅是展开,因此运行时系统并不为宏定义分配内存,但是从汇编 的角度来讲,define 却以立即数的方式保留了多份数据的拷贝。
对于 const 来说, const 是在编译期间进行处理的,const 有类型,也有类型检查,程序运行时系统会为 const 常量分配内存,而且从汇编的角度讲,const 常量在出现的地方保留的是真正数据的内存地址,只保留了一份数据的拷贝,省去了不必要的内存空间。而且,有时编译器不会为普通的 const 常量分配内存,而是直接将 const 常量添加到符号表中,省去了读取和写入内存的操作,效率更高。

C++ 中重载和重写,重定义的区别

  1. 重载,翻译自 overload,是指同一可访问区内被声明的几个具有不同参数列表的同名函数,依赖于 C++函数名字的修饰会将参数加在后面,可以是参数类型,个数,顺序的不同。根据参数列表决定调用哪个函数,重载不关心函数的返回类型。
  2. 重写,翻译自 override,派生类中重新定义父类中除了函数体外完全相同的虚函数,注意被重写的函数不能是 static 的,一定要是虚函数,且其他一定要完全相同。要注意,重写和被重写的函数是在不同的类当中的,重写函数的访问修饰符是可以不同的,尽管 virtual 中是 private 的,派生类中重写可以改为 public。
  3. 重定义(隐藏),派生类重新定义父类中相同名字的非 virtual 函数,参数列表
    和返回类型都可以不同,即父类中除了定义成 virtual 且完全相同的同名函数才
    不会被派生类中的同名函数所隐藏(重定义)。

什么情况下会调用拷贝构造函数(三种情况)
类的对象需要拷贝时,拷贝构造函数将会被调用,以下的情况都会调用拷贝构造函数:

  1. 一个对象以值传递的方式传入函数体,需要拷贝构造函数创建一个临时对象压入到栈空间中。
  2. 一个对象以值传递的方式从函数返回,需要执行拷贝构造函数创建一个临时对象作为返回值。
  3. 一个对象需要通过另外一个对象进行初始化。

构造函数为什么一般不定义为虚函数
虚函数的调用是通过实例化之后对象的虚函数表指针来找到虚函数的地址进行调用的,如果说构造函数是虚的,那么虚函数表指针则是不存在的,无法找到对应的虚函数表来调用虚函数,那么这个调用实际上也是违反了先实例化后调用的准则。

预处理,编译,汇编,链接程序的区别
一段高级语言代码经过四个阶段的处理形成可执行的目标二进制代码。
预处理器→编译器→汇编器→链接器:最难理解的是编译与汇编的区别。
这里采用《深入理解计算机系统》的说法。

  1. 预处理阶段: 写好的高级语言的程序文本比如 hello.c,预处理器根据 #开头的命令,修改原始的程序,如#include 将把系统中的头文件插入到程序文本中,通常是以 .i 结尾的文件。
  2. 编译阶段: 编译器将 hello.i 文件翻译成文本文件 hello.s,这个是汇编语言程序。高级语言是源程序。所以注意概念之间的区别。汇编语言程序是干嘛的?每条语句都以标准的文本格式确切描述一条低级机器语言指令。不同的高级语言翻译的汇编语言相同。
    1
    2
    3
    4
    # 加载依赖关系文件,处理头文件变动自动重新编译
    -include $(OBJS_CPP:.o=.d)
    $(OBJS_DIR)/%.cpp.o : ./%.cpp
    $(XX) $< -c -MMD -o $@

.d 文件是由 -MMD -MP 选项生成的。-MMD 选项告诉编译器生成依赖关系文件,而 -MP 选项告诉编译器在依赖关系文件中包含头文件的规则。然后,-include 指令将这些依赖关系文件包含在 Makefile 中。这样做的好处是,如果某个头文件被修改,相关的源文件会被重新编译,从而保持构建的一致性。

  1. 汇编阶段: 汇编器将 hello.s 翻译成机器语言指令。把这些指令打包成可重定位目标程序,即 .o文件。hello.o是一个二进制文件,它的字节码是机器语言指令,不再是字符。前面两个阶段都还有字符。
  2. 链接阶段: 比如 hello 程序调用 printf 程序,它是每个 C 编译器都会提供的标准库 C 的函数。这个函数存在于一个名叫 printf.o 的单独编译好的目标文件中,这个文件将以某种方式合并到 hello.o 中。链接器就负责这种合并。得到的是可执行目标文件。在程序编译之后,生成的目标文件中包含有待解析的未知符号。在链接过程中,链接器会根据符号表中的信息找到这些未知符号的地址,并将其添加到程序的符号表之中。链接器还会进行重定位操作,将程序中的绝对地址转换为相对地址。这个过程中,链接器会生成对全局变量、函数等符号的引用关系,并将这些符号的引用关系绑定到正确的地址上。完成链接后,生成可执行二进制文件并将其装载到内存中,程序便可以运行了。

构造函数的执行顺序?析构函数的执行顺序?
构造函数顺序

  1. 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
  2. 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。
  3. 派生类构造函数。
    析构函数顺序
  4. 调用派生类的析构函数;
  5. 调用成员类对象的析构函数;
  6. 调用基类的析构函数。
    1
    2
    3
    4
    5
    class E : public A, public B { // A->B->C->D->E
    public:
    C c;
    D d;
    };

简单说一下函数指针
从定义和用途两方面来说一下自己的理解:

  1. 首先是定义:函数指针是指向函数的指针变量。函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。
  2. 在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。
  3. 其次是用途:调用函数和做函数的参数,比如回调函数。
    示例:
    1
    2
    3
    4
    char * fun(char * p)  {…}  // 函数fun
    char * (*pf)(char * p); // 函数指针pf
    pf = fun; // 函数指针pf指向函数fun
    pf(p); // 通过函数指针pf调用函数fun

何时需要成员初始化列表?过程是什么?

  1. 当初始化一个引用成员变量时;
  2. 初始化一个 const 成员变量时;
  3. 当调用一个基类的构造函数,而构造函数拥有一组参数时;
  4. 当调用一个成员类的构造函数,而他拥有一组参数;
    list中的项目顺序是由类中的成员声明顺序决定的,不是初始化列表中的排列顺序决定的。

简述C++ 中 const 关键词

  1. const 修饰基本类型数据类型:基本数据类型,修饰符 const 可以用在类型说明符前,也可以用在类型说明符后,其结果是一样的。在使用这些常量的时候,只要不改变这些常量的值即可。
  2. const 修饰指针变量和引用变量:如果 const 位于小星星的左侧,则 const 就是用来修饰指针所指向的变量,即指针指向为常量;如果 const 位于小星星的右侧,则 const 就是修饰指针本身,即指针本身是常量。
  3. const 应用到函数中:作为参数的 const 修饰符:调用函数的时候,用相应的变量初始化 const 常量,则在函数体中,按照 const 所修饰的部分进行常量化,保护了原对象的属性。 [注意]:参数 const 通常用于参数为指针或引用的情况; 作为函数返回值的 const 修饰符:声明了返回值后,const 按照”修饰原则”进行修饰,起到相应的保护作用。
  4. const 在类中的用法:const 成员变量,只在某个对象生命周期内是常量,而对于整个类而言是可以改变的。因为类可以创建多个对象,不同的对象其 const 数据成员值可以不同。所以不能在类的声明中初始化 const 数据成员,因为类的对象在没有创建时候,编译器不知道 const 数据成员的值是什么。const 数据成员的初始化只能在类的构造函数的初始化列表中进行。const 成员函数:const 成员函数的主要目的是防止成员函数修改对象的内容。要注意,const 关键字和 static 关键字对于成员函数来说是不能同时使用的,因为 static 关键字修饰静态成员函数不含有 this 指针,即不能实例化,const 成员函数又必须具体到某一个函数。
  5. const 修饰类对象,定义常量对象:常量对象只能调用常量函数,别的成员函数都不能调用。
    补充:const 成员函数中如果实在想修改某个变量,可以使用 mutable 进行修饰。成员变量中如果想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现或者 static const。

C ++ 中的 const类成员函数(用法和意义)

  1. 常量对象可以调用类中的 const 成员函数,但不能调用非 const 成员函数; (原因:对象调用成员函数时,在形参列表的最前面加一个形参 this,但这是隐式的。this 指针是默认指向调用函数的当前对象的,所以,很自然,this 是一个常量指针 test const,因为不可以修改 this 指针代表的地址。但当成员函数的参数列表(即小括号)后加了 const 关键字(void print() const;),此成员函数为常量成员函数,此时它的隐式this形参为 const test const,即不可以通过 this 指针来改变指向对象的值。
  2. 非常量对象可以调用类中的 const 成员函数,也可以调用非 const 成员函数。

多态的实现

  1. 多态其实一般就是指继承加虚函数实现的多态,对于重载来说,实际上基于的原理是,编译器为函数生成符号表时的不同规则,重载只是一种语言特性,与多态无关,与面向对象也无关,但这又是 C++中增加的新规则,所以也算属于 C++,所以如果非要说重载算是多态的一种,那就可以说:多态可以分为静态多态和动态多态。
  2. 静态多态其实就是重载,因为静态多态是指在编译时期就决定了调用哪个函数,根据参数列表来决定;
  3. 动态多态是指通过子类重写父类的虚函数来实现的,因为是在运行期间决定调用的函数,所以称为动态多态,
  4. 一般情况下我们不区分这两个时所说的多态就是指动态多态。
  5. 动态多态的实现与虚函数表,虚函数指针相关。
  6. 扩展: 子类是否要重写父类的虚函数?子类继承父类时, 父类的纯虚函数必须重写,否则子类也是一个虚类不可实例化。 定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

new / delete ,malloc / free 区别

  1. 都可以用来在堆上分配和回收空间。new /delete 是操作符,malloc/free 是库函数。
  2. 执行 new 实际上执行两个过程:1.分配未初始化的内存空间(malloc);2.使用对象的构造函数对空间进行初始化;返回空间的首地址。如果在第一步分配空间中出现问题,则抛出 std::bad_alloc 异常,或被某个设定的异常处理函数捕获处理;如果在第二步构造对象时出现异常,则自动调用 delete 释放内存。
  3. 执行 delete 实际上也有两个过程:1. 使用析构函数对对象进行析构;2.回收内存空间(free)。
  4. 以上也可以看出 new 和 malloc 的区别,new 得到的是经过初始化的空间,而 malloc 得到的是未初始化的空间。所以 new 是 new 一个类型,而 malloc 则是malloc 一个字节长度的空间。delete 和 free 同理,delete 不仅释放空间还析构对象,delete 一个类型,free 一个字节长度的空间。
  5. 为什么有了 malloc/free 还需要 new/delete? 因为对于非内部数据类型而言,光用 malloc/free 无法满足动态对象的要求。对象在创建的同时需要自动执行构造函数,对象在消亡以前要自动执行析构函数。由于 mallo/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行的构造函数和析构函数的任务强加于 malloc/free,所以有了 new/delete 操作符。

C++ 面向对象的三大特征是:封装、继承、多态。

  1. 所谓封装
    就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让信任的类或者对象操作,对不可信的进行信息隐藏。一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
  2. 所谓继承
    是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或者“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”和“组合”来实现。
    继承概念的实现方式有两类:
    实现继承:实现继承是指直接使用基类的属性和方法而无需额外编码的能力。
    接口继承:接口继承是指仅使用属性和方法的名称、但是子类必需提供实现的能力。
  3. 所谓多态
    就是向不同的对象发送同一个消息,不同对象在接收时会产生不同的行为(即方法)。即一个接口,可以实现多种方法。
    多态与非多态的实质区别就是函数地址是早绑定还是晚绑定的。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并产生代码,则是静态的,即地址早绑定。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。

简述指针和引用的区别

  1. 指针和引用都是一种内存地址的概念,区别呢,指针是一个实体,引用只是一个别名。
  2. 在程序编译的时候,将指针和引用添加到符号表中。
  3. 指针它指向一块内存,指针的内容是所指向的内存的地址,在编译的时候,则是将“指针变量名-指针变量的地址”添加到符号表中,所以说,指针包含的内容是可以改变的,允许拷贝和赋值,有 const 和非 const 区别,甚至可以为空,sizeof 指针得到的是指针类型的大小。
  4. 而对于引用来说,它只是一块内存的别名,在添加到符号表的时候,是将”引用变量名-引用对象的地址”添加到符号表中,符号表一经完成不能改变,所以引用必须而且只能在定义时被绑定到一块内存上,后续不能更改,也不能为空,也没有 const 和非 const 区别。
  5. sizeof 引用得到代表对象的大小。而 sizeof 指针得到的是指针本身的大小。另外在参数传递中,指针需要被解引用后才可以对对象进行操作,而直接对引用进行的修改会直接作用到引用对象上。
  6. 作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引用的实质是传地址,传递的是变量的地址。

野(wild)指针与悬空(dangling)指针有什么区别?如何避免?

  1. 野指针(wild pointer):就是没有被初始化过的指针。用 gcc -Wall 编译, 会出现 used uninitialized警告。
  2. 悬空指针:是指针最初指向的内存已经被释放了的一种指针。
  3. 无论是野指针还是悬空指针,都是指向无效内存区域(这里的无效指的是”不安全不可控”)的指针。 访问”不安全可控”(invalid)的内存区域将导致”Undefined Behavior”。
  4. 如何避免使用野指针?在平时的编码中,养成在定义指针后且在使用之前完成初始化的习惯或者使用智能指针。

结构体内存对齐方式和为什么要进行内存对齐?

  1. 首先我们来说一下结构体中内存对齐的规则:
    对于结构体中的各个成员,第一个成员位于偏移为 0 的位置,以后的每个数据成员的偏移量必须是 min(#pragma pack() 制定的数,数据成员本身长度) 的倍数。
    在所有的数据成员完成各自对齐之后,结构体或联合体本身也要进行对齐,整体长度是 min(#pragma pack()制定的数,长度最长的数据成员的长度) 的倍数。
  2. 那么内存对齐的作用是什么呢?
    经过内存对齐之后,CPU 的内存访问速度大大提升。因为 CPU 把内存当成是一块一块的,块的大小可以是 2,4,8,16 个字节,因此 CPU 在读取内存的时候是一块一块进行读取的,块的大小称为内存读取粒度。比如说 CPU 要读取一个 4 个字节的数据到寄存器中(假设内存读取粒度是 4),如果数据是从 0 字节开始的,那么直接将 0-3 四个字节完全读取到寄存器中进行处理即可。
    如果数据是从 1 字节开始的,就首先要将前 4 个字节读取到寄存器,并再次读取 4-7 个字节数据进入寄存器,接着把 0 字节,5,6,7 字节的数据剔除,最后合并 1,2,3,4 字节的数据进入寄存器,所以说,当内存没有对齐时,寄存器进行了很多额外的操作,大大降低了 CPU 的性能。
    另外,还有一个就是,有的 CPU 遇到未进行内存对齐的处理直接拒绝处理,不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。所以内存对齐还有利于平台移植。
    #pragma pack(1) 是一个编译指令,用于告诉编译器取消内存对齐(或者说取消填充字节)。

简述weak_ptr

  1. weakptr 是一种不控制对象生命周期的智能指针,它指向一个 sharedptr 管理的对象。进行该对象的内存管理的是那个强引用的 shared_ptr。
  2. weakptr 只是提供了对管理对象的一个访问手段。weakptr 设计的目的是为配合 sharedptr 而引入的一种智能指针来协助 sharedptr 工作,它只可以从一个 sharedptr 或另一个 weakptr 对象构造,,它的构造和析构不会引起引用记数的增加或减少。
  3. weakptr 是用来解决 sharedptr 相互引用时的死锁问题,如果说两个 sharedptr 相互引用,那么这两个指针的引用计数永远不可能下降为0,也就是资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和 sharedptr 之间可以相互转化,sharedptr 可以直接赋值给它,它可以通过调用 lock 函数来获得sharedptr。
  4. 当两个智能指针都是 sharedptr 类型的时候,析构时两个资源引用计数会减一,但是两者引用计数还是为 1,导致跳出函数时资源没有被释放(的析构函数没有被调用),解决办法:把其中一个改为weakptr就可以。

说一下 C++ 里是怎么定义常量的?常量存放在内存的哪个位置?
对于局部常量,存放在栈区;
对于全局常量,编译期一般不分配内存,放在符号表中以提高访问效率;
字面值常量,比如字符串,放在常量区。

dynamic_cast 一般用于有继承关系的类之间的向下转换。dynamic_pointer_cast 当指针是智能指针时候,向下转换,用dynamic_Cast 则编译不能通过,此时需要使用dynamic_pointer_cast。C++基类和派生类的智能指针转换:static_pointer_cast、dynamic_pointer_cast、const_pointer_cast、reinterpret_pointer_cast

1
2
3
4
5
6
7
8
template< class T, class U >
std::shared_ptr<T> dynamic_pointer_cast( const std::shared_ptr<U>& r ) noexcept
{
if (auto p = dynamic_cast<typename std::shared_ptr<T>::element_type*>(r.get()))
return std::shared_ptr<T>{r, p};
else
return std::shared_ptr<T>{};
}

1
2
3
processWidget(std::shared_ptr<Widget>(new Widget), // potential
computePriority()); // resource
// leak!

为什么使用 std::make_unique 可以更加安全,答案是和编译器翻译代码到目标代码有关。在运行期,传递给函数的参数必须先计算,然后才发生函数调用。因此在调用processWidget时,下面的事情必须在processWidget开始执行前发生:

  1. 表达式”new Widget”必须先计算,一个Widget对象必须先创建在堆上;
  2. 负责new出来的对象指针的std::shared_ptr的构造函数必须执行;
  3. computePriority必须运行。

没有人要求编译器必须按这样的顺序来生成代码。“new Widget”必须在 std::shared_ptr构造函数前被调用,因为new的结果用作构造函数的参数,但是computePriority可以在上述调用前、后或者中间执行。也就是说编译器可能会产生这样的代码来按如下顺序执行:

  1. 执行“new Widget”;
  2. 执行computePriority;
  3. 调用std::shared_ptr构造函数。

假如这样的代码生成,在运行期,computePriority产生了异常,那么第一步生成的对象会被泄漏掉,因为没有在第3步被保存到 std::shared_ptr。

nephen wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!