嵌入式软件基础知识-C/C++

关于C/C++重点知识点汇总。

参考C/C++常见面试知识点总结附面试真题—-20190407更新


C/C++内存有哪几种类型?

  • 堆(malloc)
  • 栈(如局部变量、函数参数)
  • 程序代码区(存放二进制代码)
  • 全局/静态存储区(全局变量、static变量)和常量存储区(常量)。此外,C++中有自由存储区(new)一说。
  • 全局变量、static变量会初始化为零,而堆和栈上的变量是随机的,不确定的。

堆和栈的区别?

  • 堆存放动态分配的对象——即那些在程序运行时分配的对象,比如局部变量,其生存期由程序控制;
  • 栈用来保存定义在函数内的非static对象,仅在其定义的程序块运行时才存在;
  • 静态内存用来保存static对象,类static数据成员以及定义在任何函数外部的变量,static对象在使用之前分配,程序结束时销毁;
  • 栈和静态内存的对象由编译器自动创建和销毁。

堆和自由存储区的区别?

  • 总的来说,堆是C语言和操作系统的术语,是操作系统维护的一块动态分配内存;自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。他们并不是完全一样。
  • 从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。
  • 而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。

程序编译的过程?

  • 程序编译的过程中就是将用户的文本形式的源代码(c/c++)转化成计算机可以直接执行的机器代码的过程。主要经过四个过程:预处理、编译、汇编和链接。具体示例如下。
  • 一个hello.c的c语言程序如下。
    • 预处理阶段:hello.c–>hello.i
    • 编译阶段:hello.i–>hello.s
    • 汇编阶段:hello.s–>hello.o
    • 链接阶段:hello.o–>hello

计算机内部如何存储负数和浮点数?

  • 负数比较容易,就是通过一个标志位和补码来表示。
  • 对于浮点类型的数据采用单精度类型(float)和双精度类型(double)来存储,float数据占用32bit,double数据占用64bit,我们在声明一个变量float f= 2.25f的时候,是如何分配内存的呢?如果胡乱分配,那世界岂不是乱套了么,其实不论是float还是double在存储方式上都是遵从IEEE的规范的,float遵从的是IEEE R32.24 ,而double 遵从的是R64.53。更多可以参考浮点数表示。
  • 无论是单精度还是双精度在存储中都分为三个部分:
    • 符号位(Sign) : 0代表正,1代表为负
    • 指数位(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储
    • 尾数部分(Mantissa):尾数部分

其中float的存储方式如下图所示:
在这里插入图片描述
而双精度的存储方式如下图:


函数调用的过程?

如下结构的代码,

int main(void)
{
  ...
  d = fun(a, b, c);
  cout<<d<<endl;
  ...
  return 0;
}

调用fun()的过程大致如下:

  • main()========
    • 参数拷贝(压栈),注意顺序是从右到左,即c-b-a;
    • 保存d = fun(a, b, c)的下一条指令,即cout<<d<<endl(实际上是这条语句对应的汇编指令的起始位置);
    • 跳转到fun()函数,注意,到目前为止,这些都是在main()中进行的;
  • fun()=====
    • 移动ebp、esp形成新的栈帧结构;
    • 压栈(push)形成临时变量并执行相关操作;
    • return一个值;
    • 出栈(pop);
    • 恢复main函数的栈帧结构;
    • 返回main函数;
  • main()========

左值和右值

  • 不是很严谨的来说,左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式);右值指的则是只能出现在等号右边的变量(或表达式)。
  • 举例来说我们定义的变量 a 就是一个左值,而malloc返回的就是一个右值。或者左值就是在程序中能够寻址的东西,右值就是一个具体的真实的值或者对象,没法取到它的地址的东西(不完全准确),因此没法对右值进行赋值,但是右值并非是不可修改的,比如自己定义的class, 可以通过它的成员函数来修改右值。

什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?你通常采用哪些方法来避免和减少这类错误?

用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元即为内存泄露。

  • 使用的时候要记得指针的长度.
  • malloc的时候得确定在那里free.
  • 对指针赋值的时候应该注意被赋值指针需要不需要释放.
  • 动态分配内存的指针最好不要再次赋值.
  • 在C++中应该优先考虑使用智能指针.

C和C++的区别?

  • C++是C的超集;
  • C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制),而对于C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。

int fun() 和 int fun(void)的区别?

  • 这里考察的是c中的默认类型机制。在c中,int fun() 会解读为返回值为int(即使前面没有int,也是如此,但是在c++中如果没有返回类型将报错),输入类型和个数没有限制, 而int fun(void)则限制输入类型为一个void。
  • 在c++下,这两种情况都会解读为返回int类型,输入void类型。

const 有什么用途

  • 定义只读变量,或者常量(只读变量和常量的区别参考下面一条);
  • 修饰函数的参数和函数的返回值;
  • 修饰函数的定义体,这里的函数为类的成员函数,被const修饰的成员函数代表不能修改成员变量的值,因此const成员函数只能调用const成员函数;
  • 只读对象。只读对象只能调用const成员函数。
class Screen {
public:
const char cha; //const成员变量
char get() const; //const成员函数
};

const Screen screen; //只读对象

在C中用const能定义真正意义上的常量吗?C++中的const呢?

不能。c中的const仅仅是从编译层来限定,不允许对const 变量进行赋值操作,在运行期是无效的,所以并非是真正的常量(比如通过指针对const变量是可以修改值的),但是c++中是有区别的,c++在编译时会把const常量加入符号表,以后(仍然在编译期)遇到这个变量会从符号表中查找,所以在C++中是不可能修改到const变量的。

  • c中的局部const常量存储在栈空间,全局const常量存在只读存储区,所以全局const常量也是无法修改的,它是一个只读变量。
  • 这里需要说明的是,常量并非仅仅是不可修改,而是相对于变量,它的值在编译期已经决定,而不是在运行时决定。
  • c++中的const 和宏定义是有区别的,宏是在预编译期直接进行文本替换,而const发生在编译期,是可以进行类型检查和作用域检查的。
  • c语言中只有enum可以实现真正的常量。
  • c++中只有用字面量初始化的const常量会被加入符号表,而变量初始化的const常量依然只是只读变量。
  • c++中const成员为只读变量,可以通过指针修改const成员的值,另外const成员变量只能在初始化列表中进行初始化。

下面我们通过代码来看看区别。同样一段代码,在c编译器下,打印结果为*pa = 4, 4;在c++编译下打印的结果为 *pa = 4, 8

int main(void)
{
    const int a = 8;
    int *pa = (int *)&a;
    *pa = 4;
    printf("*pa = %d, a = %d", *pa, a);
    return 0;
}

另外值得一说的是,由于c++中const常量的值在编译期就已经决定,下面的做法是OK的,但是c中是编译通不过的。

int main(void)
{
    const int a = 8;
    const int b = 2;
    int array[a+b] = {0};
    return 0;
}

宏和内联(inline)函数的比较?

  • 首先宏是C中引入的一种预处理功能;
  • 内联(inline)函数是C++中引用的一个新的关键字;C++中推荐使用内联函数来替代宏代码片段;
  • 内联函数将函数体直接扩展到调用内联函数的地方,这样减少了参数压栈,跳转,返回等过程;
  • 由于内联发生在编译阶段,所以内联相较宏,是有参数检查和返回值检查的,因此使用起来更为安全;
  • 需要注意的是, inline会向编译期提出内联请求,但是是否内联由编译期决定(当然可以通过设置编译器,强制使用内联);
  • 由于内联是一种优化方式,在某些情况下,即使没有显示的声明内联,比如定义在class内部的方法,编译器也可能将其作为内联函数。
  • 内联函数不能过于复杂,最初C++限定不能有任何形式的循环,不能有过多的条件判断,不能对函数进行取地址操作等,但是现在的编译器几乎没有什么限制,基本都可以实现内联。

C++中有了malloc / free , 为什么还需要 new / delete?

  • malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
  • 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
  • 由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。最后补充一点体外话,new 在申请内存的时候就可以初始化(如下代码), 而malloc是不允许的。另外,由于malloc是库函数,需要相应的库支持,因此某些简易的平台可能不支持,但是new就没有这个问题了,因为new是C++语言所自带的运算符。
    int *p = new int(1);
  • 特别的,在C++中,如下的代码,用new创建一个对象(new 会触发构造函数, delete会触发析构函数),但是malloc仅仅申请了一个空间,所以在C++中引入new和delete来支持面向对象。
    #include <cstdlib>
    class Test
    {
        ...
    }
    
    Test* pn = new Test;
    Test* pm = (Test*)malloc(sizeof(Test));

C和C++中的强制类型转换?

C中是直接在变量或者表达式前面加上(小括号括起来的)目标类型来进行转换,一招走天下,操作简单,但是由于太过直接,缺少检查,因此容易发生编译检查不到错误,而人工检查又及其难以发现的情况;而C++中引入了下面四种转换:

  • static_cast
    • 用于基本类型间的转换
    • 不能用于基本类型指针间的转换
    • 用于有继承关系类对象间的转换和类指针间的转换
  • dynamic_cast
    • 用于有继承关系的类指针间的转换
    • 用于有交叉关系的类指针间的转换
    • 具有类型检查的功能
    • 需要虚函数的支持
  • reinterpret_cast
    • 用于指针间的类型转换
    • 用于整数和指针间的类型转换
  • const_cast
    • 用于去掉变量的const属性
    • 转换的目标类型必须是指针或者引用

在C++中,普通类型可以通过类型转换构造函数转换为类类型,那么类可以转换为普通类型吗?答案是肯定的。但是在工程应用中一般不用类型转换函数,因为无法抑制隐式的调用类型转换函数(类型转换构造函数可以通过explicit来抑制其被隐式的调用),而隐式调用经常是bug的来源。实际工程中替代的方式是定义一个普通函数,通过显式的调用来达到类型转换的目的。

class test{
    int m_value;
    ...
public:
    operator int()  //类型转换函数
    {
        return m_value;
    }

    int toInt() //显示调用普通函数来实现类型转换
    {
        return m_value
    }
};

int main()
{
    ...
    test a(5);
    int i = a;
    ...

    return 0;
}

static有什么用途

  • 静态(局部/全局)变量
  • 静态函数
  • 类的静态数据成员
  • 类的静态成员函数

类的静态成员变量和静态成员函数各有哪些特性?

  • 静态成员变量
    • 静态成员变量需要在类内声明(加static),在类外初始化(不能加static),如下例所示;
    • 静态成员变量在类外单独分配存储空间,位于全局数据区,因此静态成员变量的生命周期不依赖于类的某个对象,而是所有类的对象共享静态成员变量;
    • 可以通过对象名直接访问公有静态成员变量;
    • 可以通过类名直接调用公有静态成员变量,即不需要通过对象,这一点是普通成员变量所不具备的。
class example{
private:
static int m_int; //static成员变量
};

int example::m_int = 0; //没有static

cout<<example::m_int; //可以直接通过类名调用静态成员变量
  • 静态成员函数
    • 静态成员函数是类所共享的;
    • 静态成员函数可以访问静态成员变量,但是不能直接访问普通成员变量(需要通过对象来访问);需要注意的是普通成员函数既可以访问普通成员变量,也可以访问静态成员变量;
    • 可以通过对象名直接访问公有静态成员函数;
    • 可以通过类名直接调用公有静态成员函数,即不需要通过对象,这一点是普通成员函数所不具备的。
class example{
private:
static int m_int_s; //static成员变量
int m_int;
static int getI() //静态成员函数在普通成员函数前加static即可
{
  return m_int_s; //如果返回m_int则报错,但是可以return d.m_int是合法的
}
};

cout<<example::getI(); //可以直接通过类名调用静态成员变量

在C++程序中调用被C编译器编译后的函数,为什么要加extern“C”?

C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同,假设某个函数原型为:

void foo(int x, int y);

该函数被C编译器编译后在库中的名字为 _foo, 而C++编译器则会产生像: _foo_int_int 之类的名字。为了解决此类名字匹配的问题,C++提供了C链接交换指定符号 extern “C”。


头文件中的ifndef/define/endif是干什么用的? 该用法和program once的区别?

相同点:它们的作用是防止头文件被重复包含。
不同点:

  • ifndef 由语言本身提供支持,但是program once一般由编译器提供支持,也就是说,有可能出现编译器不支持的情况(主要是比较老的编译器)。
  • 通常运行速度上 ifndef 一般慢于program once,特别是在大型项目上,区别会比较明显,所以越来越多的编译器开始支持program once。
  • fndef 作用于某一段被包含(define 和 endif 之间)的代码, 而 program once 则是针对包含该语句的文件, 这也是为什么 program once 速度更快的原因。
  • 如果用 ifndef 包含某一段宏定义,当这个宏名字出现“撞车”时,可能会出现这个宏在程序中提示宏未定义的情况(在编写大型程序时特性需要注意,因为有很多程序员在同时写代码)。相反由于program once 针对整个文件, 因此它不存在宏名字“撞车”的情况, 但是如果某个头文件被多次拷贝,program once 无法保证不被多次包含,因为program once 是从物理上判断是不是同一个头文件,而不是从内容上。

当i是一个整数的时候++i和i++那个更快一点?i++和++i的区别是什么?

答:理论上++i更快,实际与编译器优化有关,通常几乎无差别。

//i++实现代码为:
int operator++(int)
{
    int temp = *this;
    ++*this;
    return temp;
}//返回一个int型的对象本身

// ++i实现代码为:
int& operator++()
{
    *this += 1;
    return *this;
}//返回一个int型的对象引用

i++和++i的考点比较多,简单来说,就是i++返回的是i的值,而++i返回的是i+1的值。也就是++i是一个确定的值,是一个可修改的左值,如下使用:

cout << ++(++(++i)) << endl;
cout << ++ ++i << endl;

可以不停的嵌套++i。这里有很多的经典笔试题,一起来观摩下:

int main()
{
    int i = 1;
    printf("%d,%d\n", ++i, ++i);    //3,3
    printf("%d,%d\n", ++i, i++);    //5,3
    printf("%d,%d\n", i++, i++);    //6,5
    printf("%d,%d\n", i++, ++i);    //8,9
    system("pause");
    return 0;
}

首先是函数的参数入栈顺序从右向左入栈的,计算顺序也是从右往左计算的,不过都是计算完以后再进行的压栈操作:

  • 对于第1个printf,首先执行++i,返回值是i,这时i的值是2,再次执行++i,返回值是i,得到i=3,将i压入栈中,此时i为3,也就是压入3,3;
  • 对于第2个printf,首先执行i++,返回值是原来的i,也就是3,再执行++i,返回值是i,依次将3,5压入栈中得到输出结果
  • 对于第3个printf,首先执行i++,返回值是5,再执行i++返回值是6,依次将5,6压入栈中得到输出结果
  • 对于第4个printf,首先执行++i,返回i,此时i为8,再执行i++,返回值是8,此时i为9,依次将i,8也就是9,8压入栈中,得到输出结果。

上面的分析也是基于VS搞的,不过准确来说函数多个参数的计算顺序是未定义的(the order of evaluation of function arguments are undefined)。笔试题目的运行结果随不同的编译器而异。


指针和引用的区别?

相同点:

  • 都是地址的概念;
  • 都是“指向”一块内存。指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名;
  • 引用在内部实现其实是借助指针来实现的,一些场合下引用可以替代指针,比如作为函数形参。

不同点:

  • 指针是一个实体,而引用(看起来,这点很重要)仅是个别名;
  • 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
  • 引用不能为空,指针可以为空;
  • “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
  • 指针和引用的自增(++)运算意义不一样;
  • 引用是类型安全的,而指针不是 (引用比指针多了类型检查)
  • 引用具有更好的可读性和实用性。

引用占用内存空间吗?

如下代码中对引用取地址,其实是取的引用所对应的内存空间的地址。这个现象让人觉得引用好像并非一个实体。但是引用是占用内存空间的,而且其占用的内存和指针一样,因为引用的内部实现就是通过指针来完成的。
比如 Type& name; <===> Type* const name。

int main(void)
{
        int a = 8;
        const int &b = a;
        int *p = &a;
        *p = 0;
        cout<<a; //output 0
    return 0;
}

三目运算符

在C中三目运算符(? :)的结果仅仅可以作为右值,比如如下的做法在C编译器下是会报错的,但是C++中却是可以是通过的。这个进步就是通过引用来实现的,因为下面的三目运算符的返回结果是一个引用,然后对引用进行赋值是允许的。

int main(void)
{
        int a = 8;
        int b = 6;
        (a>b ? a : b) = 88;
        cout<<a; //output 88
    return 0;
}

指针数组和数组指针的区别

  • 数组指针,是指向数组的指针,而指针数组则是指该数组的元素均为指针。
  • 数组指针,是指向数组的指针,其本质为指针,形式如下。如int (*p)[10],p即为指向数组的指针,()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。数组指针是指向数组首元素的地址的指针,其本质为指针,可以看成是二级指针。
    类型名 (*数组标识符)[数组长度]
    指针数组,在C语言和C++中,数组元素全为指针的数组称为指针数组,其中一维指针数组的定义形式如下。指针数组中每一个元素均为指针,其本质为数组。如int *p[n][]优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 *p=a; 这里*p表示指针数组第一个元素的值,a的首地址的值。
    类型名 *数组标识符[数组长度]

什么是面向对象(OOP)?面向对象的意义?

  • Object Oriented Programming, 面向对象是一种对现实世界理解和抽象的方法、思想,通过将需求要素转化为对象进行问题处理的一种思想。其核心思想是数据抽象、继承和动态绑定(多态)。
  • 面向对象的意义在于:将日常生活中习惯的思维方式引入程序设计中;将需求中的概念直观的映射到解决方案中;以模块为中心构建可复用的软件系统;提高软件产品的可维护性和可扩展性。

解释下封装、继承和多态?

  • 封装:
    封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
  • 继承:
    继承主要实现重用代码,节省开发时间。
    子类可以继承父类的一些东西。
    • 公有继承(public)
      公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
    • 私有继承(private)
      私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
    • 保护继承(protected)
      保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
  • 多态

什么时候生成默认构造函数(无参构造函数)?什么时候生成默认拷贝构造函数?什么是深拷贝?什么是浅拷贝?默认拷贝构造函数是哪种拷贝?什么时候用深拷贝?

  • 没有任何构造函数时,编译器会自动生成默认构造函数,也就是无参构造函数;当类没有拷贝构造函数时,会生成默认拷贝构造函数。
  • 深拷贝是指拷贝后对象的逻辑状态相同,而浅拷贝是指拷贝后对象的物理状态相同;默认拷贝构造函数属于浅拷贝。
  • 当系统中有成员指代了系统中的资源时,需要深拷贝。比如指向了动态内存空间,打开了外存中的文件或者使用了系统中的网络接口等。如果不进行深拷贝,比如动态内存空间,可能会出现多次被释放的问题。是否需要定义拷贝构造函数的原则是,是类是否有成员调用了系统资源,如果定义拷贝构造函数,一定是定义深拷贝,否则没有意义。

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

构造函数

  • 首先调用父类的构造函数;
  • 调用成员变量的构造函数;
  • 调用类自身的构造函数。
    析构函数
    对于栈对象或者全局对象,调用顺序与构造函数的调用顺序刚好相反,也即后构造的先析构。对于堆对象,析构顺序与delete的顺序相关。

C++的编译环境

如下图所示,C++的编译环境由如下几部分构成:C++标准库、C语言兼容库、编译器扩展库及编译模块。

#include<iostream>  //C++标准库,不带".h"
#include<string.h>  //C语言兼容库,由编译器厂商提供

值得注意的是,C语言兼容库功能上跟C++标准库中的C语言子库相同,它的存中主要为了兼容C语言编译器,也就是说如果一个文件只包含C语言兼容库(不包含C++标准库),那么它在C语言编译器中依然可以编译通过。


说一下static关键字的作用?

  • 全局静态变量
    • 在全局变量前加上关键字 static,全局变量就定义成一个全局静态变量。
    • 内存中位置:静态存储区,在整个程序运行期间一直存在。
    • 初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
    • 作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
  • 局部静态变量
    • 在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
    • 内存中的位置:静态存储区
    • 初始化:未经初始化的局部静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
    • 作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
  • 静态函数
    • 在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
    • 函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
    • 注意:不要在头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
  • 类的静态成员
    • 在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用。
  • 类的静态函数
    • 静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。

在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。
从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);
简洁回答:(推荐)

  • 加了static 关键字的全局变量只能在本文件中使用。
  • 例如在 a.c 中定义了static int a=10;那么在 b.c 中用extern int a 是拿不到 a 的值得,a 的作用域只在 a.c 中。
  • static定义的静态局部变量分配在数据段上,普通的局部变量分配在栈上,会因为函数栈帧的释放而被释放掉。
  • 对一个类中成员变量和成员函数来说,加了 static 关键字,则此变量/函数就没有了 this 指针了,必须通过类名才能访问

说一下const关键字的作用?

在这里插入图片描述


请说一下extern关键字的作用?

  • extern修饰变量的声明。举例来说,如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。这里需要注意的是,被引用的变量v的链接属性必须是外链接(external)的,也就是说a.c要引用到v,不只是取决于在a.c中声明extern int v,还取决于变量v本身是能够被引用到的。这涉及到c语言的另外一个话题--变量的作用域。能够被其他模块以extern修饰符引用到的变量通常是全局变量。还有很重要的一点是,extern int v可以放在a.c中的任何地方,比如你可以在a.c中的函数fun定义的开头处声明extern int v,然后就可以引用到变量v了,只不过这样只能在函数fun作用域中引用v罢了,这还是变量作用域的问题。对于这一点来说,很多人使用的时候都心存顾虑。好像extern声明只能用于文件作用域似的。
  • extern修饰函数声明。从本质上来讲,变量和函数没有区别。函数名是指向函数二进制块开头处的指针。如果文件a.c需要引用b.c中的函数,比如在b.c中原型是int fun(int mu),那么就可以在a.c中声明extern int fun(int mu),然后就能使用fun来做任何事情。就像变量的声明一样,extern int fun(int mu)可以放在a.c中任何地方,而不一定非要放在a.c的文件作用域的范围中。对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件。
  • 此外,extern修饰符可用于指示C或者C++函数的调用规范。比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。

说一下C++和C的区别

  • 设计思想上
    • C++是面向对象的语言,而C是面向过程的结构化编程语言。
  • 语法上
    • C++具有重载、继承和多态三种特性。
    • C++相比C,增加多许多类型安全的功能,比如强制类型转换。
    • C++支持范式编程,比如模板类、函数模板等。

说一说 c++ 中四种强转类型转换。

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

  • const_cast
    • 一般用于指针转换。
    • 可以将 const 指针转为非 const 指针。
    • 可以将const引用转换为非const引用。
  • static_cast
    • 用于各种隐式转换,转换之前,要先将类型转换为void,再转换成其他类型,比如非 const 转 const,void转指针等, static_cast 能用于多态向上转化,如果向下转能成功但是不安全,结果未知,因为没有动态类型检查;
  • dynamic_cast
    • 用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回 NULL,对于引用抛异常。
    • 向上转换:指的是子类向基类的转换
    • 向下转换:指的是基类向子类的转换
    • 它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
  • reinterpret_cast
    • 几乎什么都可以转,比如将 int 转指针,可能会出问题,尽量少用;

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

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


请说一下C/C++中指针和引用概念及其区别?

  • 引用
    • 引用就是 C++对 C语言的重要扩充。引用就是某一变量的一个别名,对引用的操作与对变量直接操作完全一样。
    • 引用的声明方法:类型标识符 &引用名=目标变量名;
    • 引用引入了对象的一个同义词。定义引用的表示方法与定义指针相似,只是用&代替了*。
    • 引用使用注意事项:
      • 引用必须被初始化;
      • 引用不能改变绑定的对象;
  • 指针
    • 指针利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。
    • 指针使用注意事项:
      • 初始化时要置空;
      • 使用时要考虑指向对象边界问题;
      • 不能对未初始化的指针取值或赋值;
      • 释放时要置空;
      • 如果返回动态分配内存或对象,必须使用指针;

区别:

  • 指针是一个实体,需要分配内存空间。引用只是变量的别名,不需要分配内存空间;
  • 使用 sizeof 看一个指针的大小是 4,而引用则是被引用对象的大小;
  • 指针可以被初始化为 NULL,而引用必须被初始化且必须是一个已有对象的引用;
  • 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
  • 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
  • 指针可以有多级指针(**p),而引用至多一级;
  • 指针和引用使用自增运算符的意义不一样;(指针是指向下一个空间,引用时引用的变量值加1)
  • 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

请你说一下你理解的 c++ 中的四个智能指针。

  • C++里面的四个智能指针: auto_ptr、shared_ptr、weak_ptr、unique_ptr(其中后三个是 c++11 支持,第一个已经被 11 弃用)

请回答一下数组和指针的区别。

指针和数组的主要区别如下:
在这里插入图片描述


请你回答一下野指针是什么?

野指针就是指向一个已释放的内存或者无访问权限的内存区域的指针。


请你回答一下智能指针有没有内存泄露的情况

当两个对象相互使用一个 shared_ptr 成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。


请你来说一下智能指针的内存泄漏如何解决

为了解决循环引用导致的内存泄漏,引入了 weak_ptr 弱指针,weak_ptr 的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。


请你回答一下为什么析构函数必须是虚函数?为什么 C++ 默认的析构函数不是虚函数(考点: 虚函数 析构函数)

  • 一般将基类的析构函数设置为虚函数,当我们new一个子类对象,并用基类指针指向子类对象时,避免释放基类指针后,而未释放子类对象的空间,防止内存泄漏。
  • 首先,默认的析构函数所在的类一般不会被其他类继承,而且虚函数需要额外的虚函数表和虚表指针,会占用额外的内存。所以默认构造函数不是虚函数。

请你来说一下函数指针

  • 定义:函数指针是一个指向函数首地址的指针变量。
    C在编译时,每一个函数都有一个人口地址,函数指针指向的就是这个人口地址。有了函数指针,可以通过指针来调用函数。就向指针数组一样。
  • 用途:
    调用函数和做函数的参数,比如回调函数。
  • 示例:
    char* fun(char * p) {…};  // 普通函数 fun
    char* (*pf)(char * p);     // 函数指针 pf
    pf = fun; // 函数指针 pf 指向函数 fun
    pf(p);    // 通过函数指针 pf 调用函数 fun

请你来说一下 fork 函数

fork:创建一个和当前进程映像一样的进程可以通过 fork( )系统调用;
相关头文件:

#include <sys/types.h>
#include <unistd.h>
  • 函数原型: pid_t fork(void);
  • 成功调用 fork( )会创建一个新的进程,它几乎与调用 fork( )的进程一模一样,这两个进程都会继续运行。
    返回值:
    • 在子进程中,成功的 fork( )调用会返回 0。
    • 在父进程中 fork( )返回子进程的 pid。
    • 如果出现错误,fork( )返回一个负值。
  • 最常见的 fork( )用法是创建一个新的进程,然后使用 exec( )载入二进制映像,替换当前进程的映像。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。
  • 在早期的 Unix 系统中,创建进程比较原始。当调用 fork 时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。现代的 Unix 系统采取了更多的优化,例如 Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制。

请你来说一下C++中析构函数的作用

  • 析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数,释放动态开辟的内存空间,防止内存泄漏。
  • 注: 类调用析构函数顺序:1)子类本身的析构函数;2)对象成员析构函数;3)基类析构函数。

请你来说一下静态函数和虚函数的区别

静态函数在编译的时候就已经确定运行时机(即编译时确定函数的入口地址),虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。


请你来说一说重载和覆盖。

  • 重载:其是对于一个类而言的,在类中,两个函数的函数名相同,参数列表不同(参数类型或个数),返回值无要求。
  • 覆盖:前提是子类继承父类,在父类中,有一个虚函数,然后在子类中重新定义了这个函数,这就构成了覆盖。

补充:

  • 隐藏:前提也是子类继承父类,分为两种情况。
    • 父类函数和子类函数名相同,但是参数不同,这时父类同名函数有无virtual修饰,都构成隐藏,即子类指针调用同名函数,调用的是子类的函数,父类函数被隐藏。
    • 父类函数和子类函数名相同,参数也相同,且父类同名函数无virtual修饰。

请你说一说 strcpy 和 和 strlen。

  • strcpy 是字符串拷贝函数,原型:char *strcpy(char* dest, const char *src);
  • 从src逐字节拷贝到dest,直到遇到’\0’结束,因为没有指定长度,可能会导致拷贝越界,造成缓冲区溢出漏洞,安全版本是 strncpy 函数。
  • strlen 函数是计算字符串长度的函数,返回从开始到’\0’之间的字符个数。

请你来回答一下++i 和 i++。

++i先自增1,再返回,i++先返回i,再自增1。


请你说一说你理解的虚函数和多态。

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

请你来说一说++i 和 i++ 的实现。

  • ++i 的实现
int & int::operator++()
{
  *this += 1;
  return *this;
}
  • i++的实现
const int int::operator(int)
{
  int oldvalue = *this;
  ++(*this);
  return oldvalue;
}

请你来写个函数在main函数执行前先运行。

编译器:VC++6.0
语言:C
代码:

#include<stdio.h>
int main()
{
  printf("main\n");
  return 0;
}
int before_main()
{
  printf("before_main\n");
  return 0;
}
typedef int func();
#pragma data_seg(".CRT$XIU")//用#pragma data_seg建立一个新的数据段并定义共享数据
static func * before[] = { before_main }; //定义一个函数数组,数组放的是函数的入口地址

编译器:VC++6.0
语言:C++
代码:

#include<iostream>

using namespace std;

int before_main()
{
 cout<<"before_main() called"<<endl;
 return 0;
}


int g_Value = before_main();


int main()
{
 cout<<"main() called"<<endl;
 return 0;
}

以下四行代码的区别是什么?

const char * arr ="123";
char * brr = "123";
const char crr[] ="123";
char drr[] = "123";

const char * arr = "123";
//字符串 123 保存在常量区,const 本来是修饰 arr 指向的值不能通过 arr 去修改,但是字符串"123"在常量区,本来就不能改变,所以加不加 const 效果都一样。
char * brr = "123";
//字符串 123 保存在常量区,这个 arr 指针指向的是同一个位置,同样不能通过 brr 去修改"123"的值。
const char crr[] = "123";
//这里 123 本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区。
char drr[] = "123";
//字符串 123 保存在栈区,可以通过 drr 去修改。

请你来说一下C++里是怎么定义常量的?常量存放在内存的哪个位置?

  • 常量在 C++里的定义就是一个const 加上对象类型,常量定义必须初始化。
  • 对于局部对象,常量存放在栈区。
  • 对于全局对象,编译期一般不分配内存,常量存放在全局/静态存储区。
  • 对于字面值常量,比如字符串,存放在常量存储区。

请你来说一说隐式类型转换。

首先,对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换,低精度类型会转化为高精度类型。其次,对于只存在单个参数的构造函数的对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。


请你来说一说 C++函数栈空间的最大值

Windows 默认是 2 M,不过可以调整。
Linux 默认是 8M,不过可以调整。


请你来说一说 extern “C”

C++调用 C函数需要 extern C,因为 C 语言没有函数重载。


请你回答一下 new/delete与 与 malloc/free 的区别。

首先,new/delete 是 C++的关键字,而 malloc/free 是 C语言的库函数,后者使用必须指明申请内存空间的大小,对于类类型的对象,后者不会调用构造函数和析构函数。


请你说说你了解的RTTI

RTTI (Run Time Type Identification) 指的是运行时类型识别。程序能够使用基类的指针或引来检查这些指针或引用所指的对象的实际派生类型。
RTTI提供了两个非常有用的操作符:typeid 和 dynamic_cast。

  • typeid操作符:返回指针和引用所指的实际类型,返回类型为 type_info。
    关于 typeid 的注意事项:
    • typeid 返回一个 type_info 的对象引用。
    • 如果想通过基类的指针指向派生类的数据类型,基类就必须要带有虚函数,否则,在使用typeid 时,就只能返回定义时所使用的数据类型。
    • typeid 只能获取对象的实际类型,即便这个类含有虚函数,也只能判断当前对象是基类还是派生类,而不能判断当前指针是基类还是派生类。
  • dynamic_cast操作符:将基类类型的指针或引用安全地转换为其派生类类型的指针或引用。
    关于 dynamic_cast 的注意事项:
    • 只能应用与指针和引用的转换,即只能转化为某一个类型的指针或某一个类型的引用,而不能是某类型本身。
    • 要转化的类型中必须包含虚函数,如果没有虚函数,转换就会失败。
    • 如果转换成功,返回子类的地址,如果转换失败,返回NULL。

请你说说虚函数表具体是怎样实现运行时多态的?

对于存在虚函数的基类的对象,基类对象的头部存放指向虚函数表的指针,子类若重写基类的虚函数,对应虚函数表中,该函数的地址会被重写的子类函数的地址替换。


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

每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的堆栈指针(ESP)压栈。


请你说说C语言参数压栈顺序?

根据参数列表从右向左依次压栈。


请你说说C++如何处理返回值?

生成一个临时变量,把它的引用作为函数参数传入函数内。


请你回答一下C++中拷贝构造函数的形参能否进行值传递?

不能。如果是值传递的话,调用拷贝构造函数时,首先将实参传给形参,这样又会调用拷贝构造函数,如此,会造成无限递归调用拷贝构造函数。


请你回答一下malloc与new的区别。

malloc:本身是一个函数,需要知道申请内存的大小,返回的指针需要强转。
new:本身不是函数,是一个关键字,不用指定申请内存大小,动态开辟空间,返回指针不用强转。


请你说一说select。

  • 函数原型:int select ( int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout )
  • select在使用前,先将需要监控的描述符对应的 bit 位置 1,然后将其传给 select,当有任何一个事件发生时,select 将会返回所有的描述符,需要在应用程序自己遍历去检查哪个描述符上有事件发生,效率很低,并且其不断在内核态和用户态进行描述符的拷贝,开销很大。
  • 补充:select总结

请你说说 fork,wait,exec 函数。

父进程产生子进程使用fork拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用写实拷贝机制分配内存,exec 函数可以加载一个 elf 文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。fork 从父进程返回子进程的 pid,从子进程返回 0,调用了 wait 的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回 0,错误返回-1。exec 执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1。


请你来说一下map和set有什么区别,分别又是怎么实现的?

实现机制:map 和 set 都是 C++的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和 set 所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和 set 的操作行为,都只是转调 RB-tree 的操作行为。
map和set区别在于:

  • map 中的元素是 key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;set 与之相对就是关键字的简单集合,set 中每个元素只包含一个关键字key。
  • set 的迭代器是 const 的,不允许修改元素的值;map 允许修改 value,但不允许修改 key。其原因是因为map 和 set 是根据关键字排序来保证其有序性的。
    • 对于set,如果允许修改 key 的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了 map 和 set 的结构,导致 iterator 失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以 STL 中将 set 的迭代器设置成 const,不允许修改迭代器的值;
    • 对于map,map 的迭代器则不允许修改 key 值,允许修改 value 值。
  • map 支持下标操作,set 不支持下标操作。map 可以用 key 做下标,map 的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和 mapped_type 类型默认值的元素至 map 中,因此下标运算符[ ]在 map 应用中需要慎用,const_map 不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type 类型没有默认值也不应该使用。如果 find 能解决需要,尽可能用 find。

请你来介绍一下STL的allocator(空间配置器)

  • STL的空间配置器用于封装STL容器在内存管理上的底层细节。在 C++中,其内存配置和释放如下:
  • new运算分两个阶段:
    • 调用 operator new 配置内存;
    • 调用对象构造函数构造对象内容
  • delete运算分两个阶段:
    • 调用对象析构函数;
    • 调用operator delete 释放内存

为了精密分工,STL allocator 将两个阶段操作区分开来:

  • 内存配置由alloc::allocate()函数负责,内存释放由alloc::deallocate()函数负责;
  • 对象构造由::construct()函数负责,对象析构由::destroy()函数负责。
  • 同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL 采用了两级空间配置器。
  • 当分配的空间大小超过 128B 时,会使用第一级空间配置器;
  • 当分配的空间大小小于 128B 时,将使用第二级空间配置器。
  • 第一级空间配置器直接使用 malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。

请你讲讲 STL 有什么基本组成?他们之间的关系是怎样的?

  • STL 主要由:以下六部分组成:
    • 容器
    • 迭代器
    • 仿函数
    • 算法
    • 分配器
    • 配接器
  • 他们之间的关系:
    • 分配器:给容器分配存储空间。
    • 算法:通过迭代器获取容器中的内容。
    • 仿函数:可以协助算法完成各种操作。
    • 配接器:用来套接适配仿函数。

请你说一说STL中map数据的存放形式。

对于map,数据以红黑树形式存放。
补充:
对于unordered_map,数据以哈希表形式存放。


请你说说STL中map与unordered_map

  • Map映射,map 的所有元素都是pair,同时拥有键值(key)和实值(value)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
    • 底层实现:红黑树
    • 适用场景:有序键值对不重复映射
  • Multimap
    • 多重映射。multimap 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。允许键值重复。
    • 底层实现:红黑树
    • 适用场景:有序键值对可重复映射

请你说一说vector和list的区别、应用,越详细越好

  • 概念:
    • Vector
      • 连续存储的容器,动态数组,在堆上分配空间。
      • 底层实现:数组
      • 两倍容量增长:
      • vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
      • 性能
        • 访问:O(1)
        • 插入:在最后插入(空间够):很快
        • 在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
        • 在中间插入(空间够):内存拷贝
        • 在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
        • 删除:在最后删除:很快
        • 在中间删除:内存拷贝
      • 适用场景:经常随机访问,且不经常对非尾节点进行插入删除。
    • List
      • 动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。
      • 底层:双向链表
      • 性能
        • 访问:随机访问性能很差,只能快速访问头尾节点。
        • 插入:很快,一般是常数开销
        • 删除:很快,一般是常数开销
      • 适用场景:经常插入删除大量数据
  • 区别:
    • vector 底层实现是数组;list是双向链表。
    • vector 支持随机访问,list不支持。
    • vector 是顺序内存,list不是。
    • vector 在中间节点进行插入删除会导致内存拷贝,list不会。
    • vector 一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
    • vector 随机访问性能好,插入删除性能差;list 随机访问性能差,插入删除性能好。
  • 应用
    • vector 拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用 vector。
    • list 拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用 list。

请你来说一下一个 C++ 源文件从文本到可执行文件经历的过程?

对于C++源文件,从文本到可执行文件一般需要四个过程:

  • 预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
    相关命令:gcc -E man.c -o main.i
  • 编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件。
    相关命令:gcc -S man.i -o main.s
  • 汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件。
    相关命令:gcc -c man.s -o main.o
  • 链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件。
    相关命令:gcc man.o -o main

请你来回答一下include头文件的顺序以及双引号” “和尖括号< > 的区别。

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

  • 当前头文件目录→编译器设置的头文件目录→系统变量指定的头文件路径

对于使用尖括号包含的头文件,查找头文件的路径顺序为:

  • 编译器设置的头文件路径→系统变量指定的头文件路径

什么是红黑树?