C++ 基础知识[一]

目录
Overview
c++ 八股文 第一部分

1. 基础知识(一)

1.1 C++语言的特点

①C++在C的基础上引入了面向对象机制,同时也兼容C语言;
②C++三大特性:封装继承多态
③C++程序结构清晰、易于扩充、程序可读性好;
④C++代码质量高,运行效率高、仅比汇编语言慢10%~20%;
⑥C++可复用性高,C++引入了模板的概念,有专门的模板库(STL);
⑦C++是不断发展的语言,C++11中新引入了nullptr、auto变量、Lambda匿名函数、右值引用、智能指针。

C++面向对象的三大特征

封装性: 将客观事物抽象成类,每个类对自身的数据方法实行访问控制,包括(private,protected,public)。
继承性: 广义的继承有三种实现形式:实现继承(使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。
多态性: 是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。

1.2 C++和C语言的区别

  • ① C语言是C++的子集,C++可以很好兼容C语言。但是C++又有很多新特性,如引用、智能指针、auto变量等;
  • ② C++是面对对象(object-oriented)的编程语言;C语言是面对过程(process-oriented)的编程语言;
  • ③ C语言有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄露等。而C++对此增加了不少新特性来改善安全性,如const常量、引用、cast转换、智能指针、try—catch等等;
  • ④ C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL。C++的STL库相对于C语言的函数库更灵活、更通用。

1.3 C++中 struct 和 class 的区别

① struct 一般用于描述一个数据结构集合,而 class 是强调对一个对象数据的封装
② struct 中默认的访问控制权限是 public 的,而 class 中默认的访问控制权限是 private 的;
③ 在继承关系中,struct 默认是公有继承,而 class 是私有继承
④ class关键字可以用于定义模板参数,就像typename,而 struct 不能用于定义模板参数。

1.4 include头文件的顺序以及双引号""和尖括号<>的区别

区别:
① 尖括号< >的头文件是系统文件,双引号" "的头文件是自定义文件;
② 编译器预处理阶段查找头文件的路径不一样;
查找路径:
① 使用尖括号< >(系统文件)的头文件的查找路径:编译器设置的头文件路径$\rightarrow$系统变量;
② 使用双引号" "(自定义文件)的头文件的查找路径:当前头文件目录$\rightarrow$编译器设置的头文件路径$\rightarrow$系统变量。

1.5 C++结构体和C结构体的区别

①C的结构体内不允许有函数存在,C++允许有内部成员函数,且允许该函数是虚函数;
②C的结构体对内部成员变量的访问权限只能是public,而C++允许 publicprotectedprivate三种;
③C 中使用结构体需要加上 struct 关键字,或者对结构体使用 typedef 取别名,而 C++ 中可以省略 struct 关键字直接使用;
④C语言的结构体是不可以继承的,C++的结构体是可以从其他的结构体或者类继承过来的。

1.6 导入C函数的关键字是什么,C++编译时和C有什么不同?

关键字: 在C++中,导入C函数的关键字是extern,表达形式为extern "C"extern "C" 的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

编译区别: 由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
总结: 区别在于在编译过程中是否带上函数的参数类型,C++带,C不带

1.7 简述C++从代码到可执行二进制文件的过程

预编译、编译、汇编、链接

  • 预编译:这个过程主要的处理操作如下:
    • (1) 将所有的#define删除,并且展开所有的宏定义
    • (2) 处理所有的条件预编译指令,如#if、#ifdef
    • (3) 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
    • (4) 过滤所有的注释
    • (5) 添加行号和文件名标识
  • 编译:这个过程主要的处理操作如下:
    • (1) 词法分析:将源代码的字符序列分割成一系列的记号。
    • (2) 语法分析:对记号进行语法分析,产生语法树。
    • (3) 语义分析:判断表达式是否有意义。
    • (4) 代码优化:
    • (5) 目标代码生成:生成汇编代码
    • (6) 目标代码优化
  • 汇编:这个过程主要是将汇编代码转变成机器可以执行的指令(汇编代码转为机器码)。
  • 链接:将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序(链接目标文件,形成可执行程序)。

链接分为静态链接动态链接

  • (1) 静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你再去把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。
  • (2) 动态链接,是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当你删除动态库时,可执行程序就不能运行。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。

1.8 static关键字的作用

  • 定义全局静态变量和局部静态变量:在变量前面加上static关键字。static的变量默认初始化为0。初始化的静态变量会在数据段分配内存,未初始化的静态变量会在BSS段分配内存。直到程序结束,静态变量始终会维持前值。只不过全局静态变量(在整个工程文件有效)和局部静态变量(在当前定义的文件内有效)的作用域不一样;(什么是数据段和BBS段内存分配?)
  • 定义静态函数:在函数返回类型前加上static关键字,函数即被定义为静态函数。静态函数只能在本源文件中使用;static int func()
  • ③在变量类型前加上static关键字,变量即被定义为静态变量。静态变量只能在本源文件中使用;
  • 类内静态成员变量: 在c++中,static关键字可以用于定义类中的静态成员变量:使用静态数据成员,它既可以被当成全局变量那样去存储,但又被隐藏在类的内部。类中的static静态数据成员拥有一块单独的存储区,而不管创建了多少个该类的对象。所有这些对象的静态数据成员都共享这一块静态存储空间,static修饰的变量要在类外初始化
  • 类内静态成员函数:在c++中,static关键字可以用于定义类中的静态成员函数:与静态成员变量类似,类里面同样可以定义静态成员函数。只需要在函数前加上关键字static即可。如静态成员函数也是类的一部分,而不是对象的一部分。所有这些对象的静态数据成员都共享这一块静态存储空间,只能访问类的static成员变量,static修饰的变量要在类外初始化。

1.9 数组和指针的区别

  • 概念:
    • (1)数组:数组是用于储存多个相同类型数据的集合。数组名是首元素的地址。
    • (2)指针:指针相当于一个变量,但是它和一般变量不一样,它存放的是其它变量在内存中的地址。指针名指向了内存的首地址。
  • 区别:
    • 赋值:同类型指针变量可以相互赋值;数组不行,只能一个一个元素的赋值或拷贝;
  • 存储方式:
    • 数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下标进行访问的,数组的存储空间,不是在静态区就是在栈上。
    • 指针:指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。由于指针本身就是一个变量,再加上它所存放的也是变量,所以指针的存储空间不能确定。

1.10 什么是函数指针,如何定义函数指针,有什么使用场景

  • 概念: 函数指针就是指向函数的指针变量。每一个函数都有一个入口地址,该函数入口地址就是函数指针所指向的地址。

    Note
    函数指针指向函数的入口地址!

  • 定义形式:

    1
    2
    3
    
    int func(int a); // 函数
    int (*f)(int a); // 函数指针
    f = &func;
  • 使用场景: 回调(callback)。我们调用别人提供的 API函数(Application Programming Interface,应用程序编程接口),称为Call;如果别人的库里面调用我们的函数,就叫Callback。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    //以库函数qsort排序函数为例,它的原型如下:
    void qsort(void *base,//void*类型,代表原始数组
              size_t nmemb, //第二个是size_t类型,代表数据数量
              size_t size, //第三个是size_t类型,代表单个数据占用空间大小
              int(*compar)(const void *,const void *)//第四个参数是函数指针
              );
    //第四个参数告诉qsort,应该使用哪个函数来比较元素,
    //即只要我们告诉qsort比较大小的规则,它就可以帮我们对任意数据类型的数组进行排序。
    //在库函数qsort调用我们自定义的比较函数,这就是回调的应用。
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    //示例
    int num[100];
    int cmp_int(const void* _a , const void* _b){//参数格式固定
        int* a = (int*)_a;    //强制类型转换
        int* b = (int*)_b;
        return *a - *b;  
    }
    
    qsort(num,100,sizeof(num[0]),cmp_int); //回调cmp_int函数
    

1.11 静态变量什么时候初始化

对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。
而C++标准规定:全局或静态对象当且仅当对象首次用到时才进行构造。

1.12 nullptr调用成员函数可以吗?为什么?

可以。因为在编译时对象就绑定了函数地址,和指针空不空没关系。

 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
//给出实例
class animal
{
public:
    void sleep() { cout << "animal sleep" << endl; }
    void breathe() { cout << "animal breathe haha" << endl; }

};

class fish :public animal
{
public:
    void breathe(){ cout << "fish bubble" << endl; }
};

int main()
{
    animal *pAn=nullptr;    //类指针
    pAn->breathe();   // 输出:animal breathe haha
    fish *pFish = nullptr;
    pFish->breathe(); // 输出:fish bubble
    return 0;
}

// 原因:因为在编译时对象就绑定了函数地址,和指针空不空没关系。
// pAn->breathe();编译的时候,函数的地址就和指针pAn绑定了;
// 调用breath(*this), this就等于pAn。由于函数中没有需要解引用this的地方,所以函数运行不会出错,
// 但是若用到this,因为this=nullptr,运行出错。

1.13 什么是野指针,怎么产生的,如何避免?

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的);

Note
指向位置不可知称为野指针!

产生原因:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的错误。这些我们都要注意避免。(内存泄露)

避免办法:
(1)初始化置NULL
(2)申请内存后判空
(3)指针释放后置NULL
(4)使用智能指针

1.14 静态局部变量,全局变量,局部变量的特点,以及使用场景

  • ①首先从作用域考虑:

    • C++里作用域可分为6种: 全局,局部,类,语句,命名空间和文件作用域。
      • 全局变量: 全局作用域,可以通过extern作用于其他非定义的源文件。
      • 静态全局变量: 全局作用域+文件作用域,所以无法在其他文件中使用。
      • 局部变量: 局部作用域,比如函数的参数,函数内的局部变量等等。
      • 静态局部变量: 局部作用域,只被初始化一次,直到程序结束。
  • ②从所在空间考虑:除了局部变量在栈上外,其他都在静态存储区。因为静态变量都在静态存储区,所以下次调用函数的时候还是能取到原来的值。

  • ③生命周期: 局部变量在栈上,出了作用域就回收内存;而全局变量、静态全局变量、静态局部变量都在静态存储区,直到程序结束才会回收内存。

  • ④使用场景:从它们各自特点就可以看出各自的应用场景,不再赘述。

1.15 C++继承

公有继承public:基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
私有继承private:私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
保护继承protect:保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的

1.16 常量指针和指针常量

常量指针和指针常量的区别
常量指针: 内存里的值不变
指针常量: 指针指向的内存地址不变
1
2
3
4
5
6
1. const int a;     //指的是a是一个常量,不允许修改。
2. const int *a;    //a指针所指向的内存里的值不变,即(*a)不变  常量指针
3. int const *a;    //同const int *a;

4. int *const a;    //a指针所指向的内存地址不变,即a不变     指针常量
5. const int *const a;   //都不变,即(*a)不变,a也不变

1.17 内联函数和函数的区别

  • ①内联函数比普通函数多了关键字inline;
  • ②内联函数避免了函数调用的开销;普通函数有调用的开销;
  • ③普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址
  • ④内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句(内联函数内不允许用循环语句和开关语句。普通函数没有这个要求。

1.18 简述C++有几种传值方式,之间的区别是什么?

  • 值传递、引用传递、指针传递
    • ①值传递: 形参即使在函数体内值发生变化,也不会影响实参的值;
    • ②引用传递: 形参在函数体内值发生变化,会影响实参的值;
    • ③指针传递: 在指针指向没有发生改变的前提下,形参在函数体内值发生变化,会影响实参的值;

1.19 内联函数和宏函数的区别

宏常量&宏函数

定义:

1
2
3
4
5
6
// a. 定义一个宏常量
#define MAX 1024 // 宏常量  MAX称为符号常量

// b. 定义一个宏函数
// 宏函数:宏函数就是使用宏定义定义出来的函数,并不是真正意义上的函数。
#define GETSUM(x, y) ((x) + (y)) // 宏函数

使用宏函数的注意事项:

  • 1.要保证运算的完整性;
  • 2.宏函数的使用场景:频繁调用和短小的函数,封装成宏函数;
  • 3.使用宏函数的优点:以空间换时间;

宏定义和函数的区别:

  • 1.宏在 预处理阶段完成替换,之后被替换的文本参与编译,相当于 直接插入代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数;
  • 2.宏定义属于在结构中插入代码,没有返回值; 函数调用具有返回值;
  • 3.宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型;
  • 4.宏定义不要在最后加分号;

宏定义和typedef的区别:

  • 1.宏主要用于 定义常量及书写复杂的内容; typedef主要用于 定义类型别名;
  • 2.宏替换发生在预编译阶段,属于文本插入替换;typedef是编译的一部分;
  • 3.宏不检查类型;typedef会检查数据类型;
  • 4.宏不是语句,不需要在最后加分号; typedef是语句,要加分号标识结束;
  • 5.注意对指针的操作,typedef char * p_char#define p_char char *区别巨大;

宏函数和内联函数的区别:

  • 1.在使用时,宏只做简单字符串替换(编译前或者预编译阶段)。而内联函数可以进行参数类型检查(编译时),且具有返回值;
  • 2.内联函数在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销来提高执行效率,并且进行参数类型检查,具有返回值,可以实现重载;
  • 3.宏定义时要注意书写(参数要括起来)否则容易出现歧义(保证运算的完整性),内联函数不会产生歧义;
  • 4.内联函数有类型检查、语法判断等功能,而宏没有;

define宏定义和const的区别:

  • 处理阶段: define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用;

  • 安全性:

    • 1.define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错;
    • 2.const常量有数据类型,编译器可以对其进行类型安全检查;
  • 内存占用:

    • 1.define只是将宏名称进行替换,在内存中会产生多份相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表;
    • 2.宏定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间;

1.20 四种cast类型转换

cast类型转换的作用
作用:克服c语言中强制类型转化带来的风险,C++引入了四种更加安全的强制类型转换运算符(明确转换的目的,便于程序的维护和分析)
  1. const_cast: 去除变量的const属性

    1
    2
    3
    4
    
    // 1.去除const属性,将只读变为只读写
    // 2.针对常量指针、常量引用和常量对象
    const char *p;
    char *p1 = const_cast<char*>(p);
  2. static_cast: 内置数据类型之间、基类-派生类之间的转换

    • 内置数据类型之间的转换,int转double,char转int
    • 基类指针与派生类指针之间的转换,只能转换有继承或派生关系的类。用于类层次结构之间基类和派生类指针和引用之间的转换,进行向上转型是安全的,但是进行向下转型是不安全的,但是是可以转换的;
      • 向上转型(派生类向基类转换 -> 安全):我们知道基类的引用和指针都可以指向派生类的对象,那么将派生类的指针或者引用强转为基类的指针或者引用,那么这就是向上转型,也就是向父类转;
      • 向下转型(基类向派生类转换 -> 不安全):向下转型就和向上转型相反,它是将父类的指针或者引用,强制转换为子类的指针或者引用
    • 把void类型指针转换为目标类型的指针
    • 任何类型的表达式转化为void类型
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    // 整形转浮点型
    int a = 10;
    double b = static_cast<double>(a);
    
    //基类指针转派生类
    class A{};
    class B : public A{};
    
    A *pA = new A;
    B *pB = static_cast<B*>(pA); // 向下转换不安全
    
  3. reinterpret_cast:

    • 可以将一个类型的指针转换为其它任意类型的指针,也可以用在指针和整形数据之间的转换。它是很危险的,如果我们没有使用它的充分理由,那么就不要使用它
    • 为运算对象的位模式提供较低层次上的重新解释
    • 用于底层的强制转换,依赖于机器,一般使用较少
  4. dynamic_cast: 运行时处理; 基类向派生类转换时比static_cast更安全

    • dynamic_cast运行时处理的,运行时进行类型检查,其他三种是编译时处理的
    • 不能用于内置数据类型之间的转换
    • dynamic_cast在进行向上转换时和static_cast效果是一样的,但是进行向下转换时会进行类型检查,比static_cast更加安全,下行转换是否成功取决于转换对象的实际类型与目标类型是否相同
    • 要求基类必须具有虚函数,否则编译不通过
    • 若转换成功,返回的是指向目标的指针或引用,不成功返回NULL

2. 基础知识(二)

2.1 写出 int 、bool、 float 、指针变量与 “零值”比较的if 语句

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//int与零值比较
if ( n == 0 )
if ( n != 0 )

//bool与零值比较
if (flag) // 表示flag为真
if (!flag) // 表示flag为假

//float与零值比较
const float EPSINON = 0.00001;
if ((x >= - EPSINON) && (x <= EPSINON) //其中EPSINON是允许的误差(即精度)。

//指针变量与零值比较
if (p == nullptr)
if (p != nullptr)

2.2 变量的声明和定义有什么区别

  • ① 变量的定义为变量分配地址和存储空间, 变量的声明不分配地址。
  • ② 一个变量可以在多个地方声明, 但是只在一个地方定义。声明多次,定义一次。
  • ③ 加入extern 修饰的是变量的声明,说明此变量将在文件外部或在文件后面部分定义。
  • ④ 说明:很多时候一个变量,只是声明,不分配内存空间,直到具体使用时才初始化,分配内存空间, 如外部变量。
1
2
3
4
5
6
7
8
int main()
{
	extern int A;
	//这是个声明而不是定义,声明A是一个已经定义了的外部变量
	//注意:声明外部变量时可以把变量类型去掉如:extern A;
	dosth(); //执行函数
}
int A; //是定义,定义了A为整型的外部变量

2.3 简述 #ifdef#else#endif#ifndef的作用

利用 #ifdef#endif 将某程序功能模块包括进去,以向特定用户提供该功能。在不需要时, 用户可轻易将其屏蔽。

1
2
3
4
5
6
7
8
9
  #ifdef MATH
    #include “math.c”
  #endif

  //在子程序前加上标记,以便于追踪和调试。

  #ifdef DEBUG
    printf (Indebugging!);
  #endif

应对硬件的限制。由于一些具体应用环境的硬件不一样,限于条件,本地缺乏这种设备,只能绕过硬件,直接写出预期结果。
注意:虽然不用条件编译命令而直接用if语句也能达到要求,但那样做目标程序长(因为所有语句都编译),运行时间长(因为在程序运行时间对if语句进行测试)。而采用条件编译,可以减少被编译的语句,从而减少目标程序的长度,减少运行时间。

2.4 结构体可以直接赋值吗?

  • ①结构体声明时可以直接初始化,同一结构体的不同对象之间也可以直接赋值,但是当结构体中含有指针“成员”时一定要小心。
  • ②注意:当有多个指针指向同一段内存时,某个指针释放这段内存可能会导致其他指针的非法操作。因此,在释放前一定要确保其他指针不再使用这段内存空间。

2.5 sizeof 和strlen 的区别

  • ①sizeof是一个操作符,strlen是库函数
  • ②sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为’\0’的字符串作参数。
  • ③编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。
  • ④数组(array)做sizeof的参数不退化,传递给strlen就退化为指针了

2.6 sizeof求类型大小

ref: https://www.cnblogs.com/maji233/p/11439880.html

  • ①类的大小为类的非静态成员数据的类型大小之和,也就是说静态成员数据不作考虑。 普通成员函数与sizeof无关。
  • ②虚函数由于要维护虚函数表,所以要占据一个指针大小,也就是4字节。 类的总大小也遵守类似class字节对齐的,调整规则。

ref:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
=>(32 位)
指针都是  4个字节

char     1个字节
short    2个字节
int      4个字节
long     4个字节
long int 4个字节
float    4个字节

double    8个字节
long double  8个字节

=>(64 位)
指针都是一个字长, 8个字节

char    1个字节
short   2个字节
int     4个字节

long    8个字节
long int  8个字节
double    8个字节
long double 也可以变长了, 16个字节

例如有如下结构体:

1
2
3
4
5
6
    struct Stu  //自定义的数据类型,允许用户存储不同的数据类型
    {
        int id; // 4个字节
        char sex; // 1个字节
        float hight; // 4个字节
    };

那么一个这样的结构体变量占多大内存呢?也就是 cout<<sizeof(Stu)<<endl; 会输出什么? 在了解字节对齐方式之前想当然的会以为:sizeof(Stu) = sizeof(int)+sizeof(char)+sizeof(float) = 9. 然而事实并非如此!

字节对齐原则
在系统默认的对齐方式下: 每个成员相对于这个结构体变量地址的偏移量正好是该成员类型所占字节的整数倍,且最终占用字节数为成员类型中最大占用字节数的整数倍。

在这个例子中,int id的偏移量为0(0=4x0),char sex的偏移量为4(4=1x4),float height的偏移量为8(8=2x4),此时占用12字节,也同时满足12=3x4.所以sizeof(Stu)=12.

总结:

ref:
struct地址偏移量计算

2.7 C语言的关键字static和C++的关键字static有什么区别

  • ①在 C语言 中,static 用来修饰局部静态变量和外部静态变量、函数。而 C++中除了上述功能外,还用来定义类的成员变量和函数。即静态成员变量和静态成员函数。
  • ②注意:编程时,static 的记忆性和全局性的特点可以让在不同时期调用的函数进行通信,传递信息,而 C++的静态成员则可以在多个对象实例间进行通信,传递信息。

2.8 C语言的malloc和C++中的new有什么区别

  • ①new 、delete 是操作符,可以重载,只能在C++ 中使用。
  • ②malloc、free 是函数,可以覆盖,C、C++ 中都可以使用。
  • ③new 可以调用对象的构造函数,对应的delete 调用相应的析构函数。
  • ④malloc 仅仅分配内存,free 仅仅回收内存,并不执行构造和析构函数。
  • ⑤new 、delete 返回的是某种数据类型指针,malloc、free 返回的是**void指针**。
    注意:malloc申请的内存空间要用free释放,而new申请的内存空间要用delete释放,不要混用。

ref: 2.11 new 和 malloc的区别

2.9 写一个 “标准” 宏MIN

1
#define min(a,b) ((a)<=(b)?(a):(b))

2.10 ++i和i++的区别

++i先自增1,再返回;i++先返回i,再自增1
前置版本将对象本身作为左值返回,后置版本将对象原始值的副本作为右值返回。

2.11 newmalloc的区别,各自底层实现原理(deletefree类似)

  • ①new(delete)是操作符,而malloc(free)是函数。
  • ②new在调用的时候先分配内存,再调用构造函数,释放的时候调用析构函数;而malloc没有调用构造函数和析构函数。
  • ③malloc需要给定申请内存的大小,返回的指针需要强转(返回void指针);new会调用构造函数,不用指定内存的大小,返回指针不需要强转。
  • ④new是操作符,可以被重载; malloc不行
  • ⑤new分配内存, 更直接和安全。
  • ⑥new发生错误抛出异常,malloc返回null

2.12 const 和 define 的区别

区别

  • (1)就起作用的阶段而言:#define是在编译的预处理阶段起作用,而const是在 编译运行的时候起作用。
  • (2)就起作用的方式而言:#define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误。
  • (3)就存储方式而言:#define只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份;const定义的只读变量在程序运行过程中只有一份备份。
  • (4)从代码调试的方便程度而言: const常量可以进行调试的,define是不能进行调试的,因为在预编译阶段就已经替换掉了。

const优点:

(1)const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
(2)有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
(3)const可节省空间,避免不必要的内存分配,提高效率

2.13 C++中函数指针和指针函数的区别

  1. 定义不同

指针函数: 本质是一个函数,其返回值为指针。
函数指针: 本质是一个指针,其指向一个函数。

  1. 写法不同

指针函数:int *fun(int x, int y);
函数指针:int (*fun)(int x, int y);

  1. 用法不同
 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
//指针函数示例
typedef struct _Data{
    int a;
    int b;
}Data;
//指针函数
Data* f(int a,int b){
    Data * data = new Data;
    //...
    return data;
}
int main(){
    //调用指针函数
    Data * myData = f(4,5);
    //Data * myData = static_cast<Data*>(f(4,5));
  //...
}

//函数指针示例
int add(int x,int y){
    return x+y;
}
//函数指针
int (*fun)(int x,int y);
//赋值, 函数指针指向函数add
fun = add;
//调用
cout << "(*fun)(1,2) = " << (*fun)(1,2) ;
//输出结果
//(*fun)(1,2) =  3

2.14 使用指针需要注意什么?

  • ①定义指针时,先初始化为NULL空指针。
  • ②用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
  • ③不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
  • ④避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
  • ⑤动态内存的申请与释放必须配对,防止内存泄漏。
  • ⑥用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”。

2.15 volatile有什么作用

  • ①volatile为状态寄存器一类的并行设备硬件寄存器。
  • ②一个中断服务子程序会访问到的非自动变量。
  • ③多线程间被几个任务共享的变量。 注意: 虽然volatile在嵌入式方面应用比较多,但是在PC软件的多线程中,volatile修饰的临界变量也是非常实用的。

C++中volatile的作用:
总结: 建议编译器不要对该变量进行优化,每次都从内存中读取该变量,而不是从缓存(寄存器)中读取变量。

  • volatile是“易变/不稳定”的意思。volatile是C的一个较为少用的关键字,解决变量在“共享”环境下容易出现读取错误的问题。

  • 定义为volatile的变量是说这变量可能会被意想不到地改变,即在你程序运行过程中一直会变,你希望这个值被正确地处理,每次从内存中去读这个值,而不是因编译器优化从缓存的地方读取,比如读取缓存在寄存器中的数值,从而保证volatile变量被正确的读取。

  • 在单任务的环境中,一个函数体内部,如果在两次读取变量的值之间的语句没有对变量的值进行修改,那么编译器就会设法对可执行代码进行优化。由于访问寄存器的速度要快过RAM(从RAM中读取变量的值到寄存器),以后只要变量的值没有改变,就一直从寄存器中读取变量的值,而不对RAM进行访问。

  • 而在多任务环境中,虽然在一个函数体内部,在两次读取变量之间没有对变量的值进行修改,但是该变量仍然有可能被其他的程序(如中断程序、另外的线程等)所修改。如果这时还是从寄存器而不是从RAM中读取,就会出现被修改了的变量值不能得到及时反应的问题。

2.16 一个参数可以既是const又是volatile吗

可以。用const和volatile同时修饰变量,表示这个变量在程序内部是只读的,不能改变的,只在程序外部条件变化下改变,并且编译器不会优化这个变量。每次使用这个变量时,都要小心地去内存读取这个变量的值,而不是去寄存器读取它的备份。
注意:在此一定要注意const的意思,const只是不允许程序中的代码改变某一变量,其在编译期发挥作用,它并没有实际地禁止某段内存的读写特性

2.17 *a&a有什么区别

  • &a:其含义就是“变量a的地址”。
  • *a:用在不同的地方,含义也不一样。
    • ①在声明语句中,*a只说明a是一个指针变量,如int *a
    • ②在其他语句中,*a前面没有操作数且a是一个指针时,*a代表指针a指向的地址内存放的数据(解引用),如b=*a
    • *a前面有操作数且a是一个普通变量时,a代表乘以a,如c=ba

2.18 用C 编写一个死循环程序

1
2
3
    while(1)
    {
    }

注意:很多种途径都可实现同一种功能,但是不同的方法时间和空间占用度不同,特别是对于嵌入式软件,处理器速度比较慢,存储空间较小,所以时间和空间优势是选择各种方法的首要考虑条件。

2.19 全局变量和局部变量有什么区别?是怎么实现的?操作系统和编译器是怎么知道的?

  • ①全局变量是整个程序都可访问的变量,谁都可以访问,生存期在整个程序从运行到结束(在程序结束时所占内存释放);
  • ②而局部变量存在于模块(子程序,函数)中,只有所在模块可以访问,其他模块不可直接访问,模块结束(函数调用完毕),局部变量消失,所占据的内存释放。
  • ③操作系统和编译器,可能是通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载.局部变量则分配在堆栈里面。

2.20 结构体内存对齐问题

请写出以下代码的输出结果:

 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
#include <stdio.h>

using namespace std;
/**************************************************************
*		结构体内存对⻬问题
*   从偏移为0的位置开始存储;
*	如果没有定义 #pragma pack(n)
*	sizeof 的最终结果必然是结构内部最⼤成员的整数倍,不够补⻬;
*	结构内部各个成员的⾸地址必然是⾃身⼤⼩的整数倍;
*
***************************************************************/
struct S1
{
	int i ;  //起始偏移0,sizeof(i)=4; 地址0、1、2、3分配给成员i
	char j ; //起始偏移4,sizeof(j)=1;
	int a ;	 //sizeof(a)=4,内存对齐到8个字节,从偏移量为8处存放a;
	double b;//sizeof(b)=8,内存对齐到16个字节,再存放b,结构体总大小24;
};
//结构体成员的首地址必须是自身大小的整数倍
struct S3
{
	char j;//起始偏移0,sizeof(j)=1;
	float i;//sizeof(i)=4,内存对齐到4,起始偏移量为4,再存放i
	double b;//当前地址为8,是b大小的整数倍,无需对齐,直接存放成员b 8个字节
	int a;//sizeof(a)=4,内存对齐到20,再存放a,总大小24字节;
};
int main()
{
	printf("%d\n", sizeof(S1));
	printf("%d\n", sizeof(S3));

	return 0;
}

输出:

1
2
24
24

说明:

  • ①结构体作为一种复合数据类型,其构成元素既可以是基本数据类型的变量,也可以是一些复合型类型数据。对此,编译器会自动进行成员变量的对齐以提高运算效率。
  • ②默认情况下,按自然对齐条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同,向结构体成员中size最大的成员对齐。
  • ③许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,而这个k则被称为该数据类型的对齐模数。

3 基础知识(三)

3.1 简述C、C++程序编译的内存分配情况

  • ①从静态存储区域分配:
    内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。速度快、不容易出错, 因为有系统会善后。例如全局变量,static 变量,常量字符串等。

  • ②在栈上分配:
    在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时, 这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。大小为2M。

  • ③从堆上分配:
    即动态内存分配。程序在运行的时候用 malloc 或 new 申请任意大小的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块

一个C、C++程序编译时内存分为5大存储区:堆区、栈区、全局区、文字常量区、程序代码区。

3.2 简述strcpy、sprintf 与memcpy 的区别

  • 操作对象不同,strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型, 目的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
  • 执行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。
  • 实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字符串的转化,memcpy 主要是内存块间的拷贝。
    注意:strcpy、sprintf 与memcpy 都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能。

3.3 请解析((void ()( ) )0)( )的含义

void (0)( ) :是一个返回值为void,参数为空的函数指针0。
(void ()( ))0:把0转变成一个返回值为void,参数为空的函数指针。
((void ()( ))0():在上句的基础上加括号表示整个是一个返回值为void,无参数,并且起始地址为0的函数的名字。
((void (*)( ))0)( ):这就是上句的函数名所对应的函数的调用。

3.4 typedef 和define 有什么区别

  • 用法不同
    • typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义常量,以及书写复杂使用频繁的宏。
  • 执行时间不同
    • typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
  • 作用域不同
    • typedef 有作用域限定:define 不受作用域约束,只要在define 声明后的引用都是正确的。
  • 对指针的操作不同
    • typedef 和 define 定义的指针时有很大的区别。

注意:typedef 定义是语句,因为句尾要加上分号。而define 不是语句,千万不能在句尾加分号。

3.5 指针常量与常量指针区别

  • 指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。(地址不可变)
  • 常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。(值不可变)
    • 指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。

注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。

3.6 简述队列和栈的异同

  • 队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是 “后进先出”。

注意:区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS 回收。分配方式类似于链表。 它与本题中的堆和栈是两回事。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。

3.7 设置地址为0x67a9 的整型变量的值为0xaa66

1
2
3
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa66; // 对指针取地址

注意:这道题就是强制类型转换的典型例子,无论在什么平台,地址长度和整型数据的长度是一样的, 即一个整型数据可以强制转换成地址指针类型,只要有意义即可。

3.8 编码实现字符串转化为数字

编码实现函数atoi(),设计一个程序,把一个字符串转化为一个整型数值。例如字符:“5486321 ”转化成整型:5486321。

 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
44
int myAtoi(const char * str)
{
   int num = 0; //保存转换后的数值
   int isNegative = 0; //记录字符串中是否有负号

   int n =0;
   char *p = str;
   if(p == NULL) //判断指针的合法性
   {
      return -1;
   }
   while(*p++ != '\0') //计算数字符串度
   {
      n++;
   }
   p = str;
   if(p[0] == '-') //判断数组是否有负号
   {
      isNegative = 1;
   }

   char temp = '0';
   for(int i = 0 ; i < n; i++)
   {
      char temp = *p++;
       if(temp > '9' ||temp < '0') //滤除非数字字符
      {
         continue;
      }
      if(num !=0 || temp != '0') //滤除字符串开始的0 字符
      {
         temp -= 0x30; //将数字字符转换为数值
          num += temp *int( pow(10 , n - 1 -i) );
       }
   }
   if(isNegative) //如果字符串中有负号,将数值取反
   {
       return (0 - num);
   }
   else
   {
      return num; //返回转换后的数值
   }
}

3.9 C语言的结构体(struct)和C++的类(class)有什么区别

  • ①C语言的结构体是不能有成员函数的,而C++的类可以有。
  • ②C语言的结构体中数据成员是没有private、public和protected访问限定的。而C++的类的成员有这些访问权限限定。
  • ③C语言的结构体是没有继承关系的,而C++的类却有丰富的继承关系。
  • 注意:虽然C的结构体和C++的类有很大的相似度,但是类是实现面向对象的基础。而结构体只可以简单地理解为类的前身。

3.10 简述指针常量与常量指针的区别

  • ①指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针是定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。指针常量的值只能在定义时初始化,常量指针指向一个只读的对象
  • ②指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。

注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。

3.11 哪些情况会导致“野指针”以及如何避免

  • ①指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向NULL。
  • ②指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL。
  • ③指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL。

3.12 句柄和指针的区别和联系是什么?

句柄和指针其实是两个截然不同的概念。Windows系统用句柄标记系统资源,隐藏系统的信息。你只要知道有这个东西,然后去调用就行了,它是个32bit的uint。指针则标记某个物理内存地址,两者是不同的概念。

3.13 new/delete与malloc/free的区别是什么

new能自动计算需要分配的内存空间,而malloc需要手工计算字节数。

1
2
int *p = new int[2];
int *q = (int )malloc(2sizeof(int));
  • ①new与delete直接带具体类型的指针,malloc和free返回void类型的指针。
  • ②new操作符能保证类型安全,而malloc不能。例如int *p = new float[2];就会报错;而int p = malloc(2sizeof(int))编译时编译器就无法指出错误来。
  • ③new一般分为两步:new操作和调用构造函数。new操作对应于malloc,但new操作可以重载,可以自定义内存分配策略,不做内存分配,甚至分配到非内存设备上,而malloc不行。
  • ④new调用构造函数,malloc不能;delete调用析构函数,而free不能。
  • ⑤malloc/free需要库文件stdlib.h的支持,new/delete则不需要!
  • ⑥new/delete是C++的关键字,申请内存失败时会抛出异常,malloc/free是库函数,申请内存失败后返回null。
  • ⑦new/delete是C++的内存分配和回收机制,malloc/free是C的内存分配和回收机制。

注意:delete和free被调用后,内存不会立即回收,指针也不会指向空,delete或free仅仅是告诉操作系统,这一块内存被释放了,可以用作其他用途。但是由于没有重新对这块内存进行写操作,所以内存中的变量数值并没有发生变化,出现野指针的情况。因此,释放完内存后,应该将指针指向NULL。
new delete 详解

3.14 说一说extern“C”

extern "C"的主要作用就是为了能够正确实现在C++代码中调用C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

这个功能十分有用,因为在C++出现以前,很多代码(包括很多底层的库)都是C语言写的,为了更好地支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。

在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会出现在C++的代码中调用C语言代码的情况。

3.15 请你来说一下C++中struct和class的区别

在C++中,class和struct做类型定义是只有两点区别:

  • 默认继承权限不同,class继承默认是private继承,而struct默认是public继承
  • ②class还可用于定义模板参数,像typename,但是关键字struct不能用于定义模板参数
  • ③C++保留struct关键字,原因: 保证与C语言的向下兼容性,C++必须提供一个struct
  • ④C++中的struct定义必须百分百地保证与C语言中的struct的向下兼容性,把C++中的最基本的对象单元规定为class而不是struct,就是为了避免各种兼容性要求的限制
  • ⑤对struct定义的扩展使C语言的代码能够更容易的被移植到C++中

3.16 C++类内可以定义引用数据成员吗?

可以,必须通过成员函数初始化列表初始化。

3.17 C++中类成员的访问权限

  • ①C++通过 publicprotectedprivate 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符
  • ②在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。
  • ③在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员

3.18 什么是右值引用,跟左值又有什么区别?

左值和右值的概念:

①左值:
能取地址,或者具名对象,表达式结束后依然存在的持久对象;
右值: 不能取地址,匿名对象,表达式结束后就不再存在的临时对象;
②区别:
左值能寻址,右值不能;
左值能赋值,右值不能;
左值可变,右值不能(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变);

3.19 面向对象的三大特征

  • 封装性: 将客观事物抽象成类,每个类对自身的数据和方法实行访问权限保护(private , protected , public)。
  • 继承性: 广义的继承有三种实现形式:实现继承(使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。
  • 多态性: 是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。

3.20 C++的空类有哪些成员函数

C++空类成员函数

缺省构造函数。
缺省拷贝构造函数。
缺省析构函数。
缺省赋值运算符。
缺省取址运算符。
缺省取址运算符 const 。
注意:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是空类的默认函数。另外需要注意的是,只有当实际使用这些空类成员函数的时候,编译器才会去定义它们。

4. 基础知识(四)

4.1 说一说c++中四种cast转换

C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast

  • 1、const_cast
    用于将const变量转为非const, 去除const属性

  • 2、static_cast
    用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;

  • 3、dynamic_cast
    用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的,对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。

  • 向上转换:指的是子类向基类的转换

  • 向下转换:指的是基类向子类的转换
    它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。

  • 4、reinterpret_cast
    几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;

  • 5、为什么不使用C的强制转换?

    C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

4.2 对c++中的smart pointer四个智能指针的理解:shared_ptr,unique_ptr,weak_ptr,auto_ptr

  • ①C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被C++11弃用。
  • ②智能指针的作用是管理一个指针,因为存在以下这种情况:
    • 申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
  • ③auto_ptr(c++98的方案,cpp11已经抛弃)

采用所有权模式。

1
2
3
unique_ptr p3 (new string (auto)); //#4
unique_ptr p4 //#5
p4 = p3;//此时会报错!!

编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。

另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

1
2
3
4
5
unique_ptr pu1(new string (hello world));
unique_ptr pu2;
pu2 = pu1; // #1 not allowed
unique_ptr pu3;
pu3 = unique_ptr(new string (You)); // #2 allowed

其中#1留下悬挂的 unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

注:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:

1
2
3
4
5
unique_ptr ps1, ps2;
ps1 = demo(hello);
ps2 = move(ps1);
ps1 = demo(alexia);
cout << *ps2 << *ps1 << endl;

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

成员函数:

  • use_count 返回引用计数的个数
  • unique 返回是否是独占所有权( use_count 为 1)
  • swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
  • reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
  • get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); spsp.get()是等价的

weak_ptr:

  • weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 对该对象进行内存管理的是那个强引用的shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。

  • weak_ptr 设计的目的是为了配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。

  • weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
 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
class B;
class A {
public:
  shared_ptr<B> pb_;
  ~A(){
    cout<<"A delete";
  }
};
class B {
public:
  shared_ptr<A> pa_;
  ~B(){
    cout<<"B delete";
  }
};
void fun()
{
    shared_ptr<B> pb(new B());
    shared_ptr<A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    cout << pb.use_count() << endl;
    cout << pa.use_count() << endl;
}
int main()
{
    fun();
    return 0;
}

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

注意:不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();

4.3 说说强制类型转换运算符

①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 使用:
1
2
3
4
5
6
try {
    Circle& ref_circle = dynamic_cast<Circle&>(ref_shape);
}
catch (bad_cast b) {
    cout << "Caught: " << b.what();
}

4.4 谈谈你对拷贝构造函数和赋值运算符的认识

拷贝构造函数和赋值运算符重载有以下两个不同之处:

①拷贝构造函数生成新的类对象,而赋值运算符不能。
②由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验原对象是否和新建对象相同,而赋值运算符则需要这个操作;
③另外,赋值运算中,如果原来的对象中有内存分配要先把内存释放掉。
注意:当有类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。

4.5 在C++中,使用malloc申请的内存能否通过delete释放?使用new申请的内存能否用free?

不能,malloc /free主要为了兼容C,new和delete 完全可以取代malloc /free的。
①malloc /free的操作对象都是必须明确大小的。而且不能用在动态类上
②new 和delete会自动进行类型检查和大小,malloc/free不能执行构造函数与析构函数,所以动态对象它是不行的。

当然从理论上说使用malloc申请的内存是可以通过delete释放的。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常。

4.6 用C++设计一个不能被继承的类

ref: https://blog.csdn.net/wei_cheng18/article/details/81043858

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <typename T>
class A {
  friend T;
private:
    A() {}
   ~A() {}
};
class B : virtual public A<B> {
public:
  B() {}
 ~B() {}
};
class C : virtual public B{
public:
  C() {}
 ~C() {}
};
void main( void ) {
  B b;
  //C c;
  return;
}

注意:构造函数是实现继承的关键,每次子类对象构造时,首先调用的是父类的构造函数,然后才是自己的。实现不能被继承的类关键在于想父类的构造函数使用private控制

  • 这里需要说明的是:我们设计的不能被继承的类B对基类A的继承必须是虚继承,这样一来C类继承B类时会去直接调用A的构造函数,而不是像普通继承那样,先调用B的构造函数再调用A的构造函数;

  • C类直接调用A类的构造函数,由于A类的构造函数是私有的,而B是A的友元,C类不是A的友元,友元关系不会继承,因此会编译报错。

4.8 访问基类的私有虚函数

写出以下程序的输出结果:

 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

#include <iostream.h>
class A
{
   virtual void g()
   {
      cout << "A::g" << endl;
   }
private:
   virtual void f()
   {
      cout << "A::f" << endl;
   }
};
class B : public A
{
   void g()
   {
      cout << "B::g" << endl;
   }
   virtual void h()
   {
      cout << "B::h" << endl;
   }
};
typedef void( *Fun )( void );
void main()
{
   B b;
   Fun pFun;
   for(int i = 0 ; i < 3; i++)
   {
      pFun = ( Fun )*( ( int* ) * ( int* )( &b ) + i );
      pFun();
   }
}

输出结果:

1
2
3
B::g
A::f
B::h

注意:考察了面试者对虚函数的理解程度。一个对虚函数不了解的人很难正确的做出本题。 在学习面向对象的多态性时一定要深刻理解虚函数表的工作原理。

虚函数: 通过基类访问派生类定义的函数,多态时使用,使用虚函数加上virtual关键字。
虚函数就是在基类定义一个未实现的函数名,为了提高程序的可读性
虚函数详解 C++虚函数详解_疯狂的麦克斯_max的博客-CSDN博客_c++虚函数

菱形继承1 菱形继承2

4.9 对虚函数和多态的理解

  • ①多态的实现主要分为静态多态动态多态
    • 静态多态主要是重载,在编译的时候就已经确定;
    • 动态多态是用虚函数机制实现的,在运行期间动态绑定。
    • 举个例子: 一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
  • ②虚函数的实现: 在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

4.10 简述类成员函数的重写(overwrite)、重载(overload)和隐藏的区别

(1) 重写和重载主要有以下几点不同。

  • ①范围的区别: 被重写的函数和重写的函数在两个类中,而重载和被重载的函数在同一个类中。
  • ②参数的区别: 被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一定不同。
  • ③virtual的区别:重写的基类中被重写的函数必须要有virtual修饰,而重载函数和被重载函数可以被virtual修饰,也可以没有。

(2) 隐藏和重写、重载有以下几点不同。

  • 与重载的范围不同: 和重写一样,隐藏函数和被隐藏函数不在同一个类中。
  • 参数的区别: 隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。 当参数不相同时,无论基类中的参数是否被virtual 修饰,基类的函数都是被隐藏,而不是被重写。

注意:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完全不同的,覆盖是动态绑定的多态,而重载是静态绑定的多态。

4.11 链表和数组有什么区别

存储形式:

数组是一块连续的空间,声明时就要确定长度。
链表是一块可不连续的动态空间,长度可变,每个结点要保存相邻结点指针。

数据查找:

数组的线性查找速度快,查找操作直接使用偏移地址。
链表需要按顺序检索结点, 效率低。
数据插入或删除: 链表可以快速插入和删除结点,而数组则可能需要大量数据移动。

越界问题:

链表不存在越界问题,数组有越界问题。

注意:

在选择数组或链表数据结构时,一定要根据实际需要进行选择。数组便于查询,链表便于插入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间。

4.12 用两个栈实现一个队列的功能

  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
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
typedef struct node
{
   int data;
   node *next;
}node,*LinkStack;

//创建空栈:
LinkStack CreateNULLStack( LinkStack &S)
{
 S = (LinkStack)malloc( sizeof( node ) ); // 申请新结点
 if( NULL == S)
 {
  printf("Fail to malloc a new node.\n");

  return NULL;
 }
 S->data = 0; //初始化新结点
 S->next = NULL;

 return S;
}

//栈的插入函数:
LinkStack Push( LinkStack &S, int data)
{
 if( NULL == S) //检验栈
 {
  printf("There no node in stack!");
  return NULL;
 }

 LinkStack p = NULL;
 p = (LinkStack)malloc( sizeof( node ) ); // 申请新结点

 if( NULL == p)
 {
  printf("Fail to malloc a new node.\n");
  return S;
 }
 if( NULL == S->next)
 {
  p->next = NULL;
 }
 else
 {
  p->next = S->next;
 }
 p->data = data; //初始化新结点
 S->next = p; //插入新结点
 return S;
}

//出栈函数:
node Pop( LinkStack &S)
{
 node temp;
 temp.data = 0;
 temp.next = NULL;

 if( NULL == S) //检验栈
 {
  printf("There no node in stack!");
  return temp;
 }
 temp = *S;

 if( S->next == NULL )
 {
  printf("The stack is NULL,can't pop!\n");
  return temp;
 }
 LinkStack p = S ->next; //节点出栈

 S->next = S->next->next;
 temp = *p;
 free( p );
 p = NULL;

 return temp;
}

//双栈实现队列的入队函数:
LinkStack StackToQueuPush( LinkStack &S, int data)
{
 node n;
 LinkStack S1 = NULL;
 CreateNULLStack( S1 ); //创建空栈

 while( NULL != S->next ) //S 出栈入S1
 {
  n = Pop( S );
  Push( S1, n.data );
 }
 Push( S1, data ); //新结点入栈

 while( NULL != S1->next ) //S1 出栈入S
 {
  n = Pop( S1 );
  Push( S, n.data );
 }
 return S;
}

注意:用两个栈能够实现一个队列的功能,那用两个队列能否实现一个栈的功能呢?结果是否定的,因为栈是先进后出,将两个栈连在一起,就是先进先出。而队列是现先进先出,无论多少个连在一起都是先进先出,而无法实现先进后出。

4.13 共享数据的保护

  • ①常引用: 使所引用的形参不能被更新

    1
    
    void display(const double& a);
  • ②常对象:在生存期内不能被更新,但 必须被初始化

    1
    
    A const a(3,4);
  • ③常成员函数: 不能修改对象中数据成员,也不能调用类中没有被const 修饰的成员函数(常对象唯一的对外接口).如果声明了一个常对象,则该对象只能调用他的常函数!->可以用于对重载函数的区分;

    1
    2
    
    void print();
    void print() const;
  • extern int a: 使其他文件也能访问该变量

    • 声明一个函数或定义函数时,冠以static的话,函数的作用域就被限制在了当前编译单元,当前编译单元内也必须包含函数的定义,也只在其编译单元可见,其他单元不能调用这个函数(每一个cpp文件就是一个编译单元)。

4.14 程序内存分配方式以及它们的区别

内存分配大致上可以分成5块:

  • 栈区(stack)
    • 栈,就是那些由编译器在需要时分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量函数参数等。(由编译器管理)
  • 堆区(heap)
    • 一般由程序员分配、释放,若程序员不释放,程序结束时可能由系统回收。注意,它与数据结构中的堆是两回事,分配方式类似于链表。
  • 全局区(静态区)(static)
    • 全局变量和静态变量被分配到同一块内存中。程序结束后由系统释放。
  • 常量存储区
    • 常量字符串就是放在这里的,不允许修改,程序结束后由系统释放。
  • 程序代码区
    • 存放函数体的二进制代码。

C++程序在执行时,将内存大方向划分为4个区域:

  • 程序运行前

    • 代码区:存放函数体的二进制代码,由操作系统进行管理的
    • 全局区:存放全局变量和静态变量以及常量
  • 程序运行后

    • 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
    • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

内存四区意义:

不同区域存放的数据,赋予不同的生命周期, 给我们更大的灵活编程

4.15 explicit

函数声明时加上explicit可以阻止函数参数被隐式转换

1
2
3
4
5
6
7
8
9
    Class A
    {
       explicit A(int a);
    }

    Void main()
    {
       A a1=12;   //不加explicit时会被隐式转换位 A a1=A(12);加了此时编译器会报错。
    }

被声明为explicit的构造函数通常比non-explicit 函数更受欢迎。

4.16 mutable关键字

mutable的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。
被mutable修饰的变量(mutable只能用于修饰类的非静态数据成员),将永远处于可变的状态,即使在一个const函数中。
我们知道,假如类的成员函数不会改变对象的状态,那么这个成员函数一般会声明为const。但是,有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰。(使用mutable修饰的数据成员可以被const成员函数修改)。

4.17 用const修饰函数的返回值

如果给以“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const修饰的同类型指针。例如函数:

1
2
3
4
5
  const char * GetString(void);
  // 如下语句将出现编译错误:
  char*str = GetString();
  // 正确的用法是
  const char *str =GetString();

4.18 宏、const和enum

  • #define不被视为语言的一部分。对于单纯常量,最好用const对象或者enum替换#define
  • 对于类似函数的宏,尽量使用内联函数inline替换掉#define
  • enum枚举类型是被当做 int 或者 unsigned int 类型来处理的。

4.19 static 对象和 non-local static对象区别

①C++中的static对象是指存储区不属于stack和heap、“寿命"从被构造出来直至程序结束为止的对象。
②这些对象包括全局对象,定义于namespace作用域的对象,在class、function以及file作用域中被声明为static的对象。
③其中,函数内的static对象称为local static对象,而其它static对象称为non-local static对象。

local static 对象和non-local static对象在何时被初始化(构造)这个问题上存在细微的差别:

①对于local static对象,在其所属的函数被调用之前,该对象并不存在,即只有在第一次调用对应函数时,local static对象才被构造出来。
②而对于non-local static对象,在main()函数开始前就已经被构造出来,并在main()函数结束后被析构。

建议:

1.对内置对象进行手工初始化,因为C++不保证初始化它们。
2.构造函数最好使用成员初值列,而不要在构造函数本体中使用赋值操作。初值列中列出的成员变量,其排序次序应该和它们在class中的声明次序相同(初始化顺序与声明变量顺序一致)。
3.为免除“跨编译单元的初始化次序问题”,尽量以local static对象替换non-local static对象。

4.20 全局变量和static变量的区别

①全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。
②这两者的区别在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。
③而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。
④由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用, 因此可以避免在其它源文件中引起错误。

5. 基础知识(五)

5.1 为什么栈要比堆速度要快

  • ①首先, 栈是本着LIFO原则的存储机制, 对栈数据的定位相对比较快速, 而堆则是随机分配的空间, 处理的数据比较多, 无论如何, 至少要两次定位.
  • ②其次, 栈是由CPU提供指令支持的, 在指令的处理速度上, 对栈数据进行处理的速度自然要优于由操作系统支持的堆数据.
  • ③再者, 栈是在一级缓存中做缓存的, 而堆则是在二级缓存中, 两者在硬件性能上差异巨大.
  • ④最后, 各语言对栈的优化支持要优于对堆的支持, 比如swift语言中, 三个字及以内的struct结构, 可以在栈中内联, 从而达到更快的处理速度.

5.2 c++ 析构函数调用时间

  • 对象生命周期结束,被销毁时;
  • delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类析构函数是虚函数时;
  • 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用

5.3 静态绑定 动态绑定 (也叫动态连编,静态连编)

如果父类中存在有虚函数,那么编译器便会为之生成虚表(属于类)与虚指针(属于某个对象),在程序运行时,根据虚指针的指向,来决定调用哪个虚函数,这称之与动态绑定,与之相对的是静态绑定,静态绑定在编译期就决定了。

class和template都支持接口与多态:

  • ①对classes而言,接口是显式的,以函数签名为中心。多态则是通过virtual函数(虚函数)发生于运行期;
  • ②对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。

泛型

泛型是通过参数化类型来实现在同一份代码上操作多种数据类型。利用“参数化类型”将类型抽象化,从而实现灵活的复用。

5.4 C语言的指针和C++的引用有什么区别?

  • 指针有自己的一块空间,指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元,即指针是一个实体。而引用只是一个别名;
  • 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
  • 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
  • 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;

5.5 请你说说C语言是怎么进行函数调用的

每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。(ESP(Extended Stack Pointer)为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针)

C语言参数压栈顺序?:从右到左

5.6 C++中拷贝赋值函数的形参能否进行值传递?

不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数(aa = ex.aa; //此处调用拷贝构造函数)。如此循环,无法完成拷贝,栈也会满。

5.7 include头文件的顺序以及双引号""和尖括号<>的区别

编译器预处理阶段查找头文件的路径不一样

  • 使用双引号""包含的头文件,查找头文件路径的顺序为:

    • ①当前头文件目录
    • ②编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
    • ③系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
  • 对于使用尖括号<>包含的头文件,查找头文件的路径顺序为:

    • ①编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
    • ②系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径

5.8 一个C++源文件从文本到可执行文件经历的过程

对于C/C++编写的程序,从源代码到可执行文件,一般经过下面四个步骤:

  • 预编译,预编译的时候做一些简单的文本替换,比如宏替换,而不进行语法的检查;
  • 编译,在编译阶段,编译器将检查一些语法错误,但是,如果使用的函数事先没有定义这种情况,不再这一阶段检查,编译后,得到.s文件
  • 汇编,将C/C++代码变为汇编代码,得到.o或者.obj文件
  • 链接,将所用到的外部文件链接在一起,在这一阶段,就会检查使用的函数有没有定义

链接过后,形成可执行文件.exe 详细请参阅: 一个C++源文件从文本到可执行文件经历的过程

5.9 内存泄漏原因和判断方法

内存泄漏通常是因为调用了malloc/new等内存申请操作,但是缺少了对应的free/delete。 为了判断内存是否泄漏,我们一方面可以使用Linux环境下的内存泄漏检查工具Valgrind,另一方面我们写代码的时候,可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否有泄漏。

内存泄漏分类:

  • 堆内存泄漏(heap leak)。堆内存是程序运行过程中根据需要通过malloc\realloc\new等从堆中分配的一块内存,在完成之后,必须要通过调用对应的free或者delete删除。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak。
  • 系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
  • 没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确的释放,从而造成内存泄漏。

5.10 段错误的产生原因

段错误是什么?

一句话来说,段错误是指访问的内存超出了系统给这个程序所设定的内存空间,例如访问了不存在的内存地址、访问了系统保护的内存地址、访问了只读的内存地址等等情况。这里贴一个对于“段错误”的准确定义。

段错误产生的原因

访问不存在的内存地址
访问系统保护的内存地址
访问只读的内存地址
栈溢出
详细请参阅:Linux环境下段错误的产生原因及调试方法小结

5.11 C++ 函数调用过程

总结起来整个过程就三步:

    1. 根据调用的函数名找到函数入口;
    1. 在栈中申请调用函数中的参数及函数体内定义的变量的内存空间;
    1. 函数执行完后,释放函数在栈中的申请的参数和变量的空间,最后返回值(如果有的话)。

详细请查阅:函数调用过程 / C/C++函数调用过程分析

5.12 如何调试c++多线程程序?

  1. 打印日志,日志中加上线程ID;(简单粗暴) gdb有thread相关命令,如infothread(简写infoth)显示线程消息,bxxthreadyy可以对某个thread设置断点,threadxx(简写成thrxx)切换到某个thread。再配合frame(简写f)相关的命令(比如up,down在不同frame间跳转),基本可以处理若干个不同的线程间的debug……
    详细请查阅:C++(vs)多线程调试 (转)

5.13 面向对象和面向过程的区别

  • ①面向对象方法中,把数据数据操作放在一起,组成对象; 对同类的对象抽象出其共性组成类; 类通过简单的接口与外界发生联系,对象和对象之间通过消息进行通信。
  • ②面向对象的三大特性是"封装、“多态”、“继承”,五大原则是"单一职责原则”、“开放封闭原则”、“里氏替换原则”、“依赖倒置原则”、“接口分离原则”。
  • ③而面向过程方法是以过程为中心的开发方法,它自顶向下顺序进行, 程序结构按照功能划分成若干个基本模块,这些模块形成树状结构。

(过程)优点:

  • 性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗源;比如嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
  • 缺点:没有面向对象易维护、易复用、易扩展。

(对象)优点:

  • 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统。
  • 缺点:性能比面向过程低。

5.14 关于引用赋值的多态:

1
2
3
4
5
6
7
Class B;
Class D : public B;

B& b;
D& d;
B& b1 = d ;  //父类可以作为子类的引用,此时b1表现和指针形式一致(会调用B的非虚函数)
D& d1 = b //错误,不能将子类作为父类的引用

5.15 模板的声明和实现不能分开的原因

链接的时候,需要实例化模板,这时候就需要找模板的具体实现了。假设在main函数中调用了一个模板函数,这时候就需要去实例化该类型的模板。注意main函数里面只包含了.h文件,也就是只有模板的声明,没有具体实现。就会报错。 而模板的实现.cpp里面,虽然有模板的具体实现,但是没有谁在该.cpp里面使用一个模板函数,就不会生成一个具体化的实例 详细请参阅:C++ 模板类的声明与实现分离问题 / ​​​​​​C++ 模板类的声明与实现分离问题(模板实例化)​​​​​​

5.16 C++类中引用成员和常量成员的初始化(必须用初始化列表进行初始化)

如果一个类是这样定义的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

Class A
{
  public:
      A(int pram1, int pram2, int pram3);
  private:
      int a;
      int &b; // 引用成员
      const int c; // 常量成员
}

假如在构造函数中对三个私有变量进行赋值则通常会这样写:

1
2
3
4
5
6
A::A(int pram1, int pram2, int pram3)
{
  a=pram1;
  b=pram2;
  c=pram3;
}

但是,这样是编译不过的。因为常量和引用初始化必须赋值。所以上面的构造函数的写法只是简单的赋值,并不是初始化。

正确写法应该是:

1
2
3
4
  A::A(int pram1, int pram2, int pram3):b(pram2),c(pram3)
  {
    a=pram1;
  }

采用初始化列表实现了对常量和引用的初始化。采用括号赋值的方法,括号赋值只能用在变量的初始化而不能用在定义之后的赋值。 凡是有引用类型的成员变量或者常量类型的变量的类,不能有缺省构造函数。默认构造函数没有对引用成员提供默认的初始化机制,也因此造成引用未初始化的编译错误。并且必须使用初始化列表进行初始化const对象、引用对象

5.17 memset为int型数组初始化问题

头文件: #include <string.h> memset() 函数用来将指定内存的前n个字节设置为特定的值,其原型为:

函数说明
1
void * memset(void * ptr, int value, size_t num);

参数说明:

  • ptr 为要操作的内存的指针。
  • value 为要设置的值。你既可以向 value 传递 int 类型的值,也可以传递 char 类型的值,int 和 char 可以根据 ASCII 码相互转换。
  • num 为 ptr 的前 num 个字节,size_t 就是unsigned int。

memset() 会将 ptr 所指的内存区域的前 num 个字节的值都设置为 value,然后返回指向 ptr 的指针。

无法下面这样初始化,这样的结果是a被赋值成168430090,168430090…..

1
2
int a[10]; // array
memset(a, 1, sizeof(a));

这是因为int由4个字节(说)表示,并且不能得到数组a中整数的期望值。

但我经常看到程序员使用memset将int数组元素设置为 0 或 -1。其他值不行!

1
2
3
4
5
6
7
8
9
int a[10];
int b[10];
memset(a, 0, sizeof(a));
memset(b, -1, sizeof(b));
//假设a为int型数组:
memset(a, 0x7f, sizeof(a));
//a数组每个空间将被初始化为0x7f7f7f7f,原因是C函数传参过程中的指针降级,导致sizeof(a),返回的是一个 something*指针类型大小的的字节数,如果是32位,就是4字节。所以memset按字节赋值。
memset(a,0xaf,sizeof(a));
//a数组每个空间将被初始化为0xafafafaf

5.18 编译器对 inline 函数的处理步骤

编译器对 inline 函数的处理:

  • 将 inline 函数体复制到 inline 函数调用点处;
  • 为所有 inline 函数中的局部变量分配内存空间;
  • 将 inline 函数的输入参数和返回值映射到调用方法的局部变量空间中;
  • 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO);

优点:

  • 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈栈帧开辟与回收结果返回等,从而提高程序运行速度。
  • 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
  • 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
  • 内联函数在运行时可调试,而宏定义不可以。

缺点:

  • 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  • inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
  • 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。

5.19 虚函数(virtual)可以是内联函数(inline)吗?

  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联
  • 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
  • inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译期具有实际对象而不是对象的指针或引用时才会发生;

5.20 静态库和动态库比较

静态库 (.a、.lib):

  • 将静态库的内容添加到程序中,此时程序的空间,变成了源程序空间大小 + 静态库空间大小。

动态库(共享库)(.so、.dll):

  • 常驻内存,当程序需要调用相关函数时,会从内存调用。

区别:

  • 静态库:对空间要求较低,而时间要求较高的核心程序中。(.a、.lib)
  • 动态库:对时间要求较低,对空间要求较高。(.so、.dll) 

hash

6 基础知识(六)

6.1 构造函数为什么不能定义为虚函数? ⽽析构函数⼀般写成虚函数的原因 ?

构造函数不能声明为虚函数的原因是:

  1. 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。
  2. 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。

虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。(动态绑定是根据对象的动态类型而不是函数名,在调用构造函数之前,这个对象根本就不存在,它怎么动态绑定?) 编译器在调用基类的构造函数的时候并不知道你要构造的是一个基类的对象还是一个派生类的对象。

析构函数设为虚函数的作用: 解释:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。(如果基类的析构函数不是虚函数,那么在delete 基类指针时,只调用基类的析构函数,不会调用派生类的析构函数,故派生类部分不会被析构。)

ref:
[1]. https://blog.csdn.net/Yangy_Jiaojiao/article/details/127588598
[2]. https://blog.csdn.net/Yangy_Jiaojiao/article/details/128145609

参考(待补充):
[1]. https://zhuanlan.zhihu.com/p/401341063
[2]. https://zhuanlan.zhihu.com/p/602866792
[3]. 万字长文超全 C++面经

ref: https://blog.csdn.net/m0_46245582/category_11569287.html

Buy me a coffee~
支付宝
微信
0%