C++ 基础问答


1、C和C++的区别

  1. C 面向过程,C++ 面向对象
  2. C malloc/free库函数,C++ new/delete运算符
  3. C++种新增引用、类、函数重载等概念

    2.1、#define和const的区别

  4. 编译器处理方式不同:

    1. define宏是预处理阶段,不能进行调试

    2. const常量是编译阶段
  5. 类型和安全检查不同:
    1. define宏没有类型,不做任何类型检查,仅仅代码展开,可能产生边际效应等错误

    2. const常量有具体的类型,在编译阶段会执行类型检查
  6. 存储方式不同:
    1. define 宏仅仅是代码展开,在多个地⽅进⾏字符串替换,不会分配内存,存储于程序的代 码段中,

    2. const 常量会分配内存,但只维持⼀份拷⻉,存储于程序的数据段中。
  7. 定义域不同:
    1. define 宏不受定义域限制,

    2. const常量只在定义域内有效。

      2.2、typedef 和#define 有什么区别

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

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

3、const和constexpr的区别

const 和 constexpr 变量之间的主要区别在于:

  1. const 变量的初始化可以延迟到运行时,而 constexpr 变量必须在编译时进行初始化。所有 constexpr 变量均为常量,因此必须使用常量表达式初始化。
  2. constexpr变量在编译器就能算出来。现代C++用来代替宏。

    4、inline函数的优缺点

    内联(inline)函数: 即编译器将 inline 内联函数内的代码替换到函数被调⽤的地⽅。应用于频繁调用短小函数的场景。

  3. 优点:

    1. inline函数在代码调用处展开,节省函数调用时间,提高代码执行效率;
    2. 相比于宏函数,内联函数在代码展开时,编译器会进⾏语法安全检查或数据类型转换,使⽤更加安全;
  4. 缺点:
    1. 代码膨胀,产⽣更多的开销;
    2. 如果内联函数内代码块的执⾏时间⽐调⽤时间⻓得多,那么效率的提升并没有那么⼤;
    3. 如果修改内联函数,那么所有调⽤该函数的代码⽂件都需要重新编译;
    4. 内联声明只是建议是否内联由编译器决定,所以实际并不可控。

5、悬挂指针和野指针的区别?

  1. 悬挂指针:指针释放后,未置空的指针。当指针所指向的对象被释放,但是该指针没有任何改变,以⾄于其仍然指向已经被回收的内存地址。(一般指针释放之后,及时置为nullptr);
  2. 野指针:未初始化的指针,指向一个随机地址。

如何避免悬挂指针和野指针?
声明时初始化为nullptr;释放后,及时置为nullptr;

6、变量的声明和定义?

  • 变量声明:变量不分配地址,可以在多个地方声明。
    • 加extern 修饰的是变量的声明,说明该变量将在文件以外或者文件后面部分定义。
  • 变量定义:变量分配地址和存储空间,但只能在一个地方定义(不然会重复定义)

note: 很多时候⼀个变量,只是声明不分配内存空间,直到具体使⽤时才初始化,分配内存空间, 如外部变量。

  1. int main()
  2. {
  3. extern int A;
  4. // 这是个声明⽽不是定义,声明A是⼀个已经定义了的外部变量
  5. // 注意:声明外部变量时可以把变量类型去掉如:extern A;
  6. dosth(); // 执⾏函数
  7. }
  8. int A; // 是定义,定义了A为整型的外部变量

7、static关键字

  1. 修饰局部变量时,使得该变量在静态存储区分配内存;只能在⾸次函数调⽤中进⾏⾸次初始化,之后的函数调⽤不再进⾏初始化;其⽣命周期与程序相同,但其作⽤域为局部作⽤域,并不能⼀直被访问;
  2. 修饰全局变量时,使得该变量在静态存储区分配内存;在声明该变量的整个⽂件中都是可⻅的,⽽在⽂件外是 不可⻅的;
  3. 修饰函数时,在声明该函数的整个⽂件中都是可⻅的,⽽在⽂件外是不可⻅的,从⽽可以在多⼈协作时避免同名的函数冲突;
  4. 修饰成员变量时,所有的对象都只维持⼀份拷⻉,可以实现不同对象间的数据共享;不需要实例化对象即可访问;不能在类内部初始化,⼀般在类外部初始化,并且初始化时不加 static ;
  5. 修饰成员函数时,该函数不接受this 指针,只能访问类的静态成员;不需要实例化对象即可访问。

    8、条件编译#ifdef、#else、#ifndef

  • 条件编译:按需编译目标代码。不需要的时候,可以轻易屏蔽
  • 和if语句的区别

    • if语句所有语句都编译,运行时间长。
    • 条件编译可以减少编译语句,减少目标程序的长度,减少运行时间。

      9、各数据类型(float\double)与零值比较

      1. // int
      2. if (n == 0) {}
      3. if (n != 0) {}
      4. // bool
      5. if (n) {}
      6. if (!n) {}
      7. // float
      8. // 为什么浮点数不能直接作“等值比较”?
      9. // 因为精度的原因,计算机存储的值和你理想的有偏差。
      10. const float EPSION = 0.00001; // 允许的误差(精度)
      11. if ((n >= -EPSION) && (n <= EPSION)) {}
      12. // 指针
      13. if (p == nullptr) {}
      14. if (p != nullptr) {}
  • 因为float(double)在计算机内不能精确表示,判断相等时不能采用等于符号,两数之差小于一定的精度(自设定)时就认为其相等,在比较两个浮点数的时候,尽量避免使用”==” 或者”!=”来进行直接比较,而是将其转化成”>=”或者”<=”来比较;

  • 许多论坛上经常有人问,为什么float或double型不能用来控制switch流程?答案也是因为这个,float无法作为常量值来进行比较.

    10、C++ 中Struct和Class的区别?

    主要区别是访问控制。
    C++中的struct对C中的struct进行了扩充。struct 能包含成员函数,能继承,能实现多态。

    1. 默认的继承访问控制

struct 是public ;class 是private.
所以我们在平时写类继承的时候,通常会这样写:
class B : public A
就是为了指明是public继承,而不是用默认的private继承。

    1. struct 和class 之间可以相互继承

当然,到底默认是public继承还是private继承,取决于子类而不是父类。
默认的继承访问权限是看子类到底是用的struct还是class。如下:
struct A {};
class B : A {}; // private继承
struct C : B {}; // public继承

11、C和C++中Struct的区别?

主要区别: 一个面向过程和一个面向对象
从封装、继承、多态和访问权限几个方面来说。

    1. 封装:

C的结构体是一种数据类型,只是把数据变量给包裹起来了,并不涉及算法,是一种“复合类型”,其功能基本与int ,double的用法相同,它主要解决多类型问题。
C++中是把数据变量及对这些数据变量的相关算法给封装起来,并且给对这些数据和类不同的访问权限。

    1. 继承:

C语言中不可以继承的,
C++可以继承,和类一样,实现了代码的复用。

    1. 多态:

C的结构体内不允许有函数存在,但是有默认的构造函数,就是把所有的成员属性设置为0,不能自定义。但是C的结构体是没有构造函数、析构函数、和this指针的,所以没有多态而言;
C++允许有内部成员函数,且允许该函数是虚函数可以多态。

    1. 访问权限:

C 内部成员变量的访问权限只能是public
C++允许public, protected, private三种。

12、C++中的访问权限(可见性)

  • public:类的内部和外界都可见。
  • protected:继承体系结构之间可见。
  • private:本类可见。
  • C++ 默认的访问权限(不写描述符)是private。

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

C++语言支持函数重载,C语言不支持函数重载。
函数被C++编译后在库中的名字与C语言的不同。
假设某个函数的原型为: void foo(int x, int y);
该函数被C编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。
所以C++提供了C连接交换指定符号extern“C”来解决名字匹配问题

14、C++中在main()函数之前执行些什么操作?

main函数执行之前,主要就是初始化系统相关资源:

  • 1、设置栈指针
  • 2、初始化static静态和global全局变量,即data段的内容
  • 3、将未初始化部分的全局变量赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL,等等,即.bss段的内容
  • 4、全局对象初始化,在main之前调用构造函数
  • 5、将main函数的参数,argc,argv等传递给main函数,然后才真正运行main函数

main函数执行之后:

  • 6、全局对象的析构函数会在main函数之后执行;

15、C++ 中NULL 和0的区别

NULL是空地址的意思,在使用指针变量时表示一个空地址,NULL的值在“stdio.h”头文件中被定义为一个值为0的符号常量;即#define NULL 0NULL的值为0
0可以表示数字0,也可以表示ASCII码值为0的字符
“”表示字符串的长度为0的字符串。
千万别把NULL与0等同起来。
无论C还是C++,NULL都是一个implementation-defined的宏,是实现相关的。NULL并不就是0,也不是(void)0。只不过,大多数编译器把NULL定义为0或者(void)0而已,但并非所有编译器都这样做的。
一些常用的代码例如if (fpFile = fopen(....) == ....)应该显式地跟NULL比较,而不要只写if (fpFile = fopen(....)),这是不良代码。

16、sizeof和strlen的区别?

  • sizeof是C/C++中的一个操作符,其作用就是返回一个对象或者类型所占的内存字节数。其参数可以是数据的类型,也可以是变量。

    strlen是库函数,作用是计算给定字符串的长度,不包括’\0’在内。

  • 编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。

  • 数组做sizeof的参数不退化,传递给strlen就退化为指针了

    1. char name1[10]; // 定义全局性数组 系统默认初始化为'\0'
    2. int main()
    3. {
    4. char name[10];
    5. printf("%d\n" , sizeof(name)); // 10
    6. printf("%d\n" , strlen(name)); // 随机值
    7. printf("%d\n" , sizeof(name1)); // 10
    8. printf("%d\n" , strlen(name1)); // 0
    9. system("pause" );
    10. return 0;
    11. }
    12. // 定义在全局的数组,即使你没有初始化,系统也会默认初始化为0,而
    13. // name1在这里是char类型,所以编译器会自动把它初始化为"\0".
    14. // sizeof(name)依旧算的是数组的大小,而strlen是遇到"\0"就结束
    15. // 由于name没有初始化,strlen(name)的结果是个随机值,什么时候遇到"\0",就什么时候停下来。

    17、数组名的退化

    无论是整型数组还是字符数组,数组名作为右值的时候都代表数组首元素的首地址。
    数组发生降级(数组名退化为数组首元素的地址)的情况:数组传参、数组名参与运算
    数组名不会发生降级的情况:sizeof(数组名)、取地址数组名(取到的是整个数组的地址而不是首元素的地址)

    1. int a[] = { 1, 2, 3, 4 };
    2. printf("%d\n", sizeof(a + 0)); // 4 数组名a参与运算发生了降级,变为首元素的地址,a+0依旧是首元素的地址,相当于求sizeof(&a[0]) ,而一个地址本身是四个字节
    3. printf("%d\n", sizeof(*a)); // 4 对首元素的地址进行解引用取到首元素的值,为int型
    4. printf("%d\n", sizeof(a + 1)); // 4 sizeof(&a[1])
    5. printf("%d\n", sizeof(a[1])); // 4 数组的每个元素都是整型
    6. printf("%d\n", sizeof(&a)); // 4 取到整个数组的地址(地址为四个字节存储)
    7. printf("%d\n", sizeof(&a + 1)); // 4 地址的大小为四个字节
    8. printf("%d\n", sizeof(&a[0])); // 4 地址的大小为四个字节
    9. printf("%d\n", sizeof(&a[0] + 1)); // 4 地址的大小为四个字节
    10. printf("%d\n", sizeof(*&a)); // 16 &a取到整个数组的地址,再解引用取到整个数组

    ```cpp void example(char welcome[]) { printf(“%d”, sizeof(welcome)); return; } int main() { char welcome[] = “Welcome to Test”; example(welcome); // 输出4 printf(“\n%d\n”, sizeof(welcome)); // 输出16, 包括’\0’ return 0;

// 原因:当数组当作参数传递时,它就退化成指针了, // 要求数组长度的话,可以在main函数内部求得 }

  1. ```cpp
  2. char name[] = "abcdef" ; // 6个字符还有一个"\0"
  3. printf("%d\n" , sizeof(name[0])); // 1 name[0]='a' char一个字节存储
  4. printf("%d\n" , sizeof(&name)); // 4 取到整个数组的地址 地址为四字节存储
  5. printf("%d\n" , sizeof(*name)); // 1
  6. printf("%d\n" , sizeof(&name + 1)); // 4 地址! (把整个数组都跳过去了)
  7. printf("%d\n" , sizeof(name + 1)); // 4 数组名参与运算降级为地址 ==> sizeof(&a[1])
  8. printf("%d\n" , sizeof(name)); // 10 数组的大小
  9. printf("%d\n" , strlen(name)); // 6 遇到'\0'就结束
  10. printf("%d\n" , strlen(&name)); // 6
  11. //printf("%d\n", strlen(*name)); // 无效,不能strlen字符
  12. printf("%d\n" , strlen(&name + 1)); // 随机值
  13. printf("%d\n" , strlen(name + 1)); // 5 为跳过首元素后的"bcdef"的长度
  14. // strlen(&name) :strlen函数一个字符一个字符跳转,直到遇到'\0'才结束。 这里编译器进行隐式的强制类型转换成char*,相当于在求strlen(name)
  15. // strlen(&name + 1):这是一个随机值,因为&name + 1把整个数组都跳过去了,传给strlen的参数是name数组后面未可知的地址,strlen会一直走下去,直到遇到"\0"
  16. // sizeof(*name):name发生降级,变为首元素的首地址,再解引用取到字符'a'(*name='a'),输出1

18、关键字explicit

出现这个关键字的原因,是在C++中有这样规定的基础上:当定义了只有一个参数的构造函数时,同时也定义了一种隐式的类型转换
C++中的关键字explicit主要是用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。类构造函数默认情况下声明为隐式的即implicit。
explicit关键字的作用:就是防止类构造函数的隐式自动转换
explicit关键字只对有**一个参数**的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了。
在C++中, 如果的构造函数只有一个参数时, 那么在编译的时候就会有一个缺省的转换操作。将该构造函数对应数据类型的数据转换为该类对象。

class String {
public:
    String(int size);
    String(const char *p);
}

String str1(24);  // ok.
String str2 = 10; // ok. 
String str3;       // notok, 没有默认构造函数
String str4("aa"); // ok
String str5 = "bbb"; // ok
String str6 = 'c';   // ok
str1 = 3; // ok.赋值
str3 = str1; // ok, 至少编译没有问题

// 解析:
String str2 = 10;
// 在C++中, 如果的构造函数只有一个参数时, 那么在编译的时候就会有一个缺省的转换操作:将该构造函数对应数据类型的数据转换为该类对象. 这段代码, 编译器自动将整型转换为String类对象, 实际上等同于下面的操作:
String str2(10);
or
String tmp(10);
String str2 = tmp;
class String {
public:
    explicit String(int size);
    String(const char *p);
}

String str1(24);  // ok.
String str2 = 10; // notok. 
String str3;       // notok, 没有默认构造函数
String str4("aa"); // ok
String str5 = "bbb"; // ok
String str6 = 'c';   // notok
str1 = 3; // notok
str3 = str1; // notok, 除非类实现操作运算符=

19、volatile修饰符的作用

  • 状态寄存器一类的并行设备硬件寄存器
  • 一个中断服务子程序会访问到的非自动变量
  • 多线程间被几个任务共享的变量

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

20、操作系统和编译器是怎么知道的全局变量和局部变量的?

可能通过内存分配的位置

  • 全局变量分配在全局数据段并且在程序开始运行的时候被加载。
  • 局部变量分配在堆栈。

    21、简述strcpy、sprintf 与memcpy 的区别

  • 操作对象不同,strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型, ⽬的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。

  • 执⾏效率不同,memcpy 最⾼,strcpy 次之,sprintf 的效率最低。
  • 实现功能不同,strcpy 主要实现字符串变量间的拷⻉,sprintf 主要实现其他数据类型格式到字 符串的转化,memcpy 主要是内存块间的拷⻉。

「注意」:strcpy、sprintf 与memcpy 都可以实现拷⻉的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷⻉功能。

22、请解析((void ()( ) )0)( )的含义

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

23、指针和应用的区别?

  • 指针有⾃⼰的⼀块空间,⽽引⽤只是⼀个别名;
  • 使⽤sizeof看⼀个指针的⼤⼩是4,⽽引⽤则是被引⽤对象的⼤⼩;
  • 作为参数传递时,指针需要被解引⽤才可以对对象进⾏操作,⽽直接对引⽤的修改都会改变引⽤所指向的对象;
  • 可以有const指针,但是没有const引⽤;
  • 指针在使⽤中可以指向其它对象,但是引⽤只能是⼀个对象的引⽤,不能被改变;
  • 指针可以有多级指针(**p),⽽引⽤⽌于⼀级;
  • 指针和引⽤使⽤++运算符的意义不⼀样;
  • 如果返回动态内存分配的对象或者内存,必须使⽤指针,引⽤可能引起内存泄露

24、句柄和指针对区别?

句柄和指针时两个不同的概念。

  • 系统用句柄标记系统资源,隐藏系统的信息,通过句柄找到该资源的位置,他是一个uint_t的值。
  • 指针则标记某个物理地址。

    25、说⼀说extern“C”

    extern “C”的主要作⽤:就是为了能够正确实现C++代码调⽤其他C语⾔代码。
    加上extern “C”后,会指示编译器这部分代码按C语⾔(⽽不是C++)的⽅式进⾏编译。由于C++⽀持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,⽽不仅仅是函数名;⽽C语⾔并不⽀持函数重载,因此编译C语⾔代码的函数时不会带上函数的参数类型,⼀般只包括函数名。
    这个功能⼗分有⽤处,因为在C++出现以前,很多代码都是C语⾔写的,⽽且很底层的库也是C语⾔写的,为了更好的⽀持原来的C代码和已经写好的C语⾔库,需要在C++中尽可能的⽀持C,⽽extern “C”就是其中的⼀个策略。

  • C++代码调⽤C语⾔代码

  • 在C++的头⽂件中使⽤
  • 在多个⼈协同开发时,可能有的⼈⽐较擅⻓C语⾔,⽽有的⼈擅⻓C++,这样的情况下也会有⽤到