Cpp八股文(1)

1、在main执行之前和之后执行的代码可能是什么?

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

​ 设置栈指针-------------操作系统或启动文件(如crt0.s/WinMainCRTStartup)会配置CPU的栈指针(SP寄存器),为主线程分配初始栈空间

​ 初始化静态、全局变量或者说是分配内存空间

​ 将main函数的参数argc,argv等传递给main函数,然后才真正运行main函数

​ __attribute__((constructor))---被该参数标记的函数会优先与main函数执行

main函数执行之后

​ 资源释放--动态库卸载、全局对象析构、文件描述符/内存清理等收尾操作

​ 全局对象的析构函数

​ 可以用 atexit 注册一个函数,它会在main 之后执行;

​ __attribute__((destructor))--标记的函数会在main()返回后或exit()调用时执行

2、结构体内存对齐目的?对齐的方式有哪些?

目的:1. 提高结构体成员的访问速度 2. 提高内存利用率。

自然对齐:

  1. 成员偏移量是其大小整数倍,不够就在其前面补padding;
  2. 结构体整体大小,是最大成员大小的整数倍,不够就在末尾补padding。
  3. \#pragma pack(push, n) \#pragma pack(pop) 或者 \#pragma pack(n)
  4. 成员偏移量是min(成员大小,n)的整数倍,不够在其前补padding;
  5. 结构体整体大小,是n的整数倍,不够就在末尾补padding。

alignas(n)

结构体整体大小,是n的整数倍,不够在末尾补padding。

总结:#pragma pack可以增大、减小结构体大小,一般用于减小;

alignas只能增加,若比自然对齐系数还小,编译器会报错(clang)。

确定结构体中每个元素大小可以通过下面这种方法:

3、指针和引用的区别

指针是一种变量类型,引用更像是现有变量的别名,它不能绑定未初始化变量,绑定后不可更改。

引用一般不占用额外内存。在汇编实现上,引用跟指针一样,但编译器会对此进行优化,尽量替换为对原始变量的直接访问,使得不会增加内存开销。

定义、双级性、初始化、空值、sizeof、作为参数、安全性

4、在传递函数参数时,什么时候该使用指针,什么时候该使用引用呢?

参数可能为空

参数为C数组:通常用指针来传递数组起始地址,因为数组名会隐形转换成数组首元素的const指针

别的情况都尽量使用引用

5、堆和栈的区别

管理方式堆中资源由程序员控制(容易产生memory leak)栈资源由编译器自动管理,无需手工控制
内存管理机制系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删 除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确释放本内存空间,另外系统会将多余的部分重新放入空闲链表中)只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示栈溢出。(这一块理解一下链表和队列的区别,不连续空间和连续空间的区别,应该就比较好 理解这两种机制的区别了)
空间大小堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit 系统理论上是4G),所以堆的空间比较灵活,比较大栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在 编译时确定,VC中可设置)
碎片问题对于堆,频繁的new/delete会造成大量碎片,使程序效率降低对于栈,它是有点类似于数据结构上的一个先进后出的栈,进出一一对应,不会产生碎片。(看到这里我突然明白了为什么面试官在问我堆和栈的区别之前先问了我栈和队列的区别)
生长方向堆向上,向高地址方向增长。栈向下,向低地址方向增长。
分配方式堆都是动态分配(没有静态分配的堆)栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现。
分配效率堆由C/C++函数库提供,机制很复杂。所以堆的效率比栈低很多。栈是其系统提供的数据结构,计算机在底层对栈提供支持,分配专门 寄存器存放栈地址,栈操作有专门指令。

6、你觉得堆快一点还是栈快一点?

毫无疑问是栈快一点。

因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。

而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢

7、区别以下指针类型?

int *p[10]  
int (*p)[10]  
int *p(int)  
int (*p)(int)

int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。表示一个数组是由指针类型的变量构成的

int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。表示这个指针指向一个int类型的数组

int p(int)是函数声明,函数名是p,参数是int类型的,返回值是int 类型的。

int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。

8、new / delete 与 malloc / free的异同以及底层原理----见第二简历!!(非常重要)

13、宏定义和函数有何区别?

宏在编译时完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数。

宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。

宏定义不要在最后加分号。

14、宏定义和typedef区别?

宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。

宏替换发生在预处理阶段,属于文本插入替换;typedef作用在编译阶段。

宏不检查类型;typedef会检查类型。

注意对指针的操作,typedef char p_char和#define p_char char 区别巨大。

#define p_char char*  
p_char a, b; // a是char指针, b是char

15、变量声明和定义区别?

声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间。

相同变量可以在多处声明(外部变量extern),但只能在一处定义。

16、strlen和sizeof区别?

sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。

sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是'0'的字符串。

因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。

int main(int argc, char const *argv[]){  
const char* str = "name";  
sizeof(str); // 取的是指针str的长度,是8  
strlen(str); // 取的是这个字符串的长度,不包含结尾的 0。大小是4  
return 0;  
}

17、常量指针和指针常量区别?

指针常量是一个指针,读成常量的指针,指向一个只读变量,如int const p或const int p。

常量指针:指向常量的指针,指针指向的数据不能被修改,但指针本身可以指向另一个地址。

,如int *const p。

update1:https://www.nowcoder.com/discuss/597948 ,网友“ 牛客191489444号 ”指出笔误,感谢!

18、a和&a有什么区别?

C++
假设数组int a[10];int (*p)[10] = &a;

数组名是const 指针,int *const, 其值不可修改,即不能执行a++等操作。

&a是一个指向数组的指针,其类型为int (*)[10](就是前面提到的数组指针),其加1时,系统会认为是数组首地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后一个元素的地址。(位移整个数组。

19、C++和Python的区别

包括但不限于:

Python是一种脚本语言,是解释执行的,而C++是编译语言,是需要编译后在特定平台运行的。python可以很方便的跨平台,但是效率没有C++高。

Python使用缩进来区分不同的代码块,C++使用花括号来区分

C++中需要事先定义变量的类型,而Python不需要,Python的基本数据类型只有数字,布尔值,字符串,列表,元组等等

Python的库函数比C++的多,调用起来很方便

20、C++和C语言的区别

z

21、C++与Java的区别

语言特性

Java语言给开发人员提供了更为简洁的语法;完全面向对象,由于JVM可以安装到任何的操作系统上,所以说它的可移植性强

Java语言中没有指针的概念,引入了真正的数组。不同于C++中利用指针实现的“伪数组”,Java引入了真正的数组,同时将容易造成麻烦的指针从语言中去掉,这将有利于防止在C++程序中常见的因为数组操作越界等指针操作而对系统数据进行非法读写带来的不安全问题

C++也可以在其他系统运行,但是需要不同的编码(这一点不如Java,只编写一次代码,到处运行),例如对一个数字,在windows下是大端存储,在unix中则为小端存储。Java程序一般都是生成字节码,在JVM里面运行得到结果

Java用接口(Interface)技术取代C++程序中的抽象类。接口与抽象类有同样的功能,但是省却了在实现和维护上的复杂性

垃圾回收

C++用析构函数回收垃圾,写C和C++程序时一定要注意内存的申请和释放

Java语言不使用指针,内存的分配和回收都是自动进行的,程序员无须考虑内存碎片的问题

应用场景

Java在桌面程序上不如C++实用,C++可以直接编译成exe文件,指针是c++的优势,可以直接对内存的操作,但同时具有危险性 。(操作内存的确是一项非常危险的事情,一旦指针指向的位置发生错误,或者误删除了内存中某个地址单元存放的重要数据,后果是可想而知的)

Java在Web 应用上具有C++ 无可比拟的优势,具有丰富多样的框架

对于底层程序的编程以及控制方面的编程,C++很灵活,因为有句柄的存在

update1:微信好友“宇少”进行“多继承”->"抽象类"的勘误

22、C++中struct和class的区别

相同点

两者都拥有成员函数、公有和私有部分任何可以使用class完成的工作,同样可以使用struct完成

不同点

两者中如果不对成员不指定公私有,struct默认是公有的,class则默认是私有的

class默认是private继承,而struct模式是public继承

引申:C++和C的struct区别

C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)

C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数

C++中,struct增加了访问权限,且可以和类一样有成员函数,成员默认访问说明符为public(为了与C兼容)

struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class{};);C++中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例

23、define宏定义和const的区别

编译阶段

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

安全性

define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错

const常量有数据类型,编译器可以对其进行类型安全检查

内存占用

define只是将宏名称进行替换,在内存中会产生多分相同的备份。const在程序运行中只有一份备份。

24、C++中const和static的作用!!!!

static

const

25、C++的顶层const和底层const

概念区分

顶层const:指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是 * 号的右边

底层const:指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是 * 号的左边

举个例子

int a = 10;int* const b1 = &a; //顶层const,b1本身是一个常量  
const int* b2 = &a; //底层const,b2本身可变,所指的对象是常量  
const int b3 = 20; //顶层const,b3是常量不可变  
const int* const b4 = &a; //前一个const为底层,后一个为顶层,b4不可变  
const int& b5 = a; //用于声明引用变量,都是底层const

区分作用

执行对象拷贝时有限制,常量的底层const不能赋值给非常量的底层const

使用命名的强制类型转换函数const_cast时,只能改变运算对象的底层const

const int a;int const a;const int *a;int *const a;

int const a和const int a均表示定义常量类型a。

const int *a,其中a为指向int型变量的指针,const在 * 左侧,表示a指向不可变常量。(看成const (*a),对引用加const)

int *const a,依旧是指针类型,表示a为指向整型数据的常指针。(看成const(a),对指针const)

26、数组名和指针(这里为指向数组首元素的指针)区别?

二者均可通过增减偏移量来访问数组中的元素。

数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。

当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。

27、final和override关键字

override

当在父类中使用了虚函数时候,你可能需要在某个子类中对这个虚函数进行重写,以下方法都可以:

class A{  
    virtual void foo();  
};  
class B : public A{  
    void foo(); //OK  
    virtual void foo(); // OK  
    void foo() override; //OK  
}

如果不使用override,当你手一抖,将foo()写成了f00()会怎么样呢?结果是编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。如果这个虚函数很重要的话,那就会对整个程序不利。所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的:

class A{  
    virtual void foo();  
};  
class B : public A{  
    virtual void f00(); //OK,这个函数是B新增的,不是继承的  
    virtual void f0o() override; //Error, 加了override之后,这个函数一定是继承自A的,A找不到就报错  
};

final

当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。例子如下:

C++

class Base{  
virtual void foo();  
};  
class A : public Base{  
void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写  
};  
class B final : A // 指明B是不可以被继承的  
{ void foo() override; // Error: 在A中已经被final了  
};  
class C : B // Error: B is final{};

28、拷贝初始化和直接初始化

当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正

在创建的对象。举例如下

string str1("I am a string");//语句1 直接初始化  
string str2(str1);  
//语句2 直接初始化,str1是已经存在的对象,直接调用构造函数对str2进行初始化  
string str3 = "I am a string";  
//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,  
//再把临时对象作为参数,使用拷贝构造函数构造str3  
string str4 = str1;  
//语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数

为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就完全等价于直接初始化了

(语句1和语句3等价),但是需要辨别两种情况。

当拷贝构造函数为private时:语句3和语句4在编译时会报错

使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译时会报错

29、初始化和赋值的区别

对于简单类型来说,初始化和赋值没什么区别

对于类和复杂数据类型来说,这两者的区别就大了,举例如下:

class A{  
public:  
    int num1;  
    int num2;  
public:  
    A(int a=0, int b=0):num1(a),num2(b){};  
    A(const A& a){};  
    //重载 = 号操作符函数  
    A& operator=(const A& a){  
        num1 = a.num1 + 1;  
        num2 = a.num2 + 1;  
        return *this;  
    };  
};  
int main(){  
    A a(1,1);  
    A a1 = a; //拷贝初始化操作,调用拷贝构造函数  
    A b;  
    b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2  
    return 0;  
}

30、extern"C"的用法

为了能够正确的在C++代码中调用C语言的代码:在程序中加上extern "C"后,相当于告诉编译器这部分代码是C语言写的,因此要按照C语言进行编译,而不是C++;

哪些情况下使用extern "C":

(1)C++代码中调用C语言代码;

(2)在C++中的头文件中使用;

(3)在多个人协同开发时,可能有人擅长C语言,而有人擅长C++;

举个例子,C++中调用C代码:

#ifndef __MY_HANDLE_H__  
#define __MY_HANDLE_H__  
extern "C"{  
    typedef unsigned int result_t;  
    typedef void* my_handle_t;  
    my_handle_t create_handle(const char* name);  
    result_t operate_on_handle(my_handle_t handle);  
    void close_handle(my_handle_t handle);  
}
#endif

综上,总结出使用方法,在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。所以使用extern "C"全部都放在于cpp程序相关文件或其头文件中。

总结出如下形式:

(1)C++调用C函数:

//xx.h  
extern int add(...)  
//xx.c  
int add(){ }  
//xx.cpp  
extern "C" { #include "xx.h"}

(2)C调用C++函数

//xx.h  
extern "C"{ int add();}  
//xx.cpp  
int add(){ }  
//xx.c  
extern int add();

31、野指针和悬空指针

都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致未定义行为。

野指针
野指针,指的是没有被初始化过的指针

int main(void) {  
int* p; // 未初始化  
std::cout<< *p << std::endl; // 未初始化就被使用  
return 0;}

因此,为了防止出错,对于指针初始化时都是赋值为 nullptr,这样在使用时编译器就会直接报错,产生非法内存访问

悬空指针
悬空指针,指针最初指向的内存已经被释放了的一种指针

int main(void) {  
int * p = nullptr;  
int* p2 = new int;  
p = p2;  
delete p2;  
}

此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr。此时再使用,编译器会直接保错。 避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。

产生原因及解决办法:

野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。

悬空指针:指针free或delete之后没有及时置空 => 释放操作后立即置空。

32、C和C++的类型安全

什么是类型安全?

类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。好的程序员可以使用类型不那么安全的语言写出类型相当安全的程序,相反的,差一点儿的程序员可能使用类型相当安全的语言写出类型不太安全的程序。绝对类型安全的编程语言暂时还没有。

(1)C的类型安全

C只在局部上下文中表现出类型安全,比如试图从一种结构体的指针转换成另一种结构体的指针时,编译器将会报告错误,除非使用显式类型转换。然而,C中相当多的操作是不安全的。以下是两个十分常见的例子:

printf格式输出

上述代码中,使用%d控制整型数字的输出,没有问题,但是改成%f时,明显输出错误,再改成%s时,运行直接报segmentation fault错误

malloc函数的返回值

malloc是C中进行内存分配的函数,它的返回类型是void_即空类型指针,常常有这样的用法char_ pStr=(char)malloc(100sizeof(char)),这里明显做了显式的类型转换。

类型匹配尚且没有问题,但是一旦出现int pInt=(int)malloc(100*sizeof(char))就很可能带来一些问题,而这样的转换C并不会提示错误。

(2)C++的类型安全

如果C++使用得当,它将远比C更有类型安全性。相比于C语言,C++提供了一些新的机制保障类型安全:

操作符new返回的指针类型严格与对象匹配,而不是void*

C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;

引入const关键字代替#define constants,它是有类型、有作用域的,而#define constants只是简单的文本替换

一些#define宏可被改写为inline函数,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保证类型安全

C++提供了dynamic_cast关键字,使得转换过程更加安全,因为dynamic_cast比static_cast涉及更多具体的类型检查。

例1:使用void*进行类型转换

例2:不同类型指针之间转换

#include<iostream>
using namespace std;  
class Parent{};  
class Child1 : public Parent{  
public:  
    int i;  
    Child1(int e):i(e){}  
};  
class Child2 : public Parent{  
public:  
    double d;  
    Child2(double e):d(e){}  
};  
int main(){  
    Child1 c1(5);  
    Child2 c2(4.1);  
    Parent* pp;  
    Child1* pc1;  
    pp=&c1; 
    }

上面两个例子之所以引起类型不安全的问题,是因为程序员使用不得当。第一个例子用到了空类型指针void,第二个例子则是在两个类型指针之间进行强制转换。因此,想保证程序的类型安全性,应尽量避免使用空类型指针void,尽量不对两种类型指针做强制转换。

33、C++中的重载、重写(覆盖)和隐藏的区别

(1)重载(overload)

重载是指在同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。重载和函数成员是否是虚函数无关。举个例子:

C++  
class A{  
...  
virtual int fun();  
void fun(int);  
void fun(double, double);  
static int fun(char);  
...  
};

(2)重写(覆盖)(override)

重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体要求基类函数必须是虚函数且:

与基类的虚函数有相同的参数个数

与基类的虚函数有相同的参数类型

与基类的虚函数有相同的返回值类型

举个例子:

Plaintext

//父类
class A{
public: 
    virtual int fun(int a){}
}//子类
class B : public A{
public: //重写,一般加override可以确保是重写父类的函数 
    virtual int fun(int a) override{}
}

重载与重写的区别:

重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系

重写要求参数列表相同,重载则要求参数列表不同,返回值不要求

重写关系中,调用方法根据对象类型决定,重载根据调用时实参表与形参表的对应关系来选择函数体

(3)隐藏(hide)

隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数,包括以下情况:

两个函数参数相同,但是基类函数不是虚函数。和重写的区别在于基类函数是否是虚函数。举个例子:

//父类  
class A{  
public:  
    void fun(int a){ cout << "A中的fun函数" << endl; }  
};  
//子类  
class B : public A{  
public:  
    //隐藏父类的fun函数  
    void fun(int a){ cout << "B中的fun函数" << endl; }  
};  
int main(){  
    B b;  
    b.fun(2); //调用的是B中的fun函数  
    b.A::fun(2); //调用A中fun函数  
    return 0;  
}

两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中。举个例子:

//父类  
class A{  
public:  
virtual void fun(int a){ cout << "A中的fun函数" << endl; }  
};  
//子类  
class B : public A{  
public:  
//隐藏父类的fun函数  
virtual void fun(char* a){ cout << "A中的fun函数" << endl; }  
};  
int main(){  
B b;  
b.fun(2); //报错,调用的是B中的fun函数,参数类型不对  
b.A::fun(2); //调用A中fun函数  
return 0;  
}

评论区 0