语法八股-01

知识列表

  1. main函数执行之前和之后执行的代码可能是什么
  2. 结构体内存对齐问题
  3. 指针和引用的区别
  4. 在传递参数时,什么时候传递引用,什么时候传递指针
  5. 堆和栈的区别
  6. new/delete 与malloc/free的异同
  7. new和delete是如何实现的
  8. 被free回收的内存是立即返回给操作系统吗
  9. 宏定义和函数有何区别
  10. 宏定义和typedef区别
  11. 变量声明和定义区别
  12. strlen和sizeof区别
  13. struct和class的区别
  14. define宏定义和const的区别
  15. const和static的作用
  16. 数组名和指针的区别
  17. final和override

1.main函数执行之前和之后执行的代码可能是什么

在main函数执行之前
  1. 设置栈指针
  2. 初始化static变量和global全局变量,即.data段的内容
  3. 将未初始化的全局变量赋初值,即.bss段的内容
  4. 全局对象初始化,在main之前调用构造函数
  5. 将main函数的参数argcargv等传递给main函数,然后才真正执行main函数
  6. __attribute__((constrcutor)),这是GCC编译器提供的一个特性,用于指定一个函数在程序启动无需再main函数中显示调用这些函数void init() __attribute__((constructor));

    void init() {
       std::cout << “This is the initialization function.” << std::endl;
    }
在main函数执行之后
  1. 全局对象的析构函数会在main函数之后执行
  2. 可以用atexit注册一个函数,它会在main之后执行
  3. __attribute__((destructor))

2.结构体内存对齐问题

结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同

未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐

3.指针和引用的区别

指针是一个变量,用于存储一个地址;引用跟原来的变量实质上是同一个东西,是原变量的别名

指针可以有多级,引用只有一级

指针可以为空,引用不能为NULL且在定义时必须初始化

指针在初始化后可以改变指向,而引用在初始化之后不可再改变

sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小

当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,二者指向的地址相同,但是是两个指针变量,除了生命周期不同,而引用是同一个变量,可以直接修改

在汇编层面,一些编译器将引用当成指针操作,因此引用会占用空间,是否占用空间,应当结合编译器分析

4.在传递参数时,什么时候传递引用,什么时候传递指针

需要返回函数内局部变量的内存的时候用指针,需要指针传参需要开辟内存,用完记得释放指针

对栈空间使用比较敏感的时候使用引用,使用引用传递不需要创建临时变量,开销较小

类对象作为参数传递的时候使用引用

5.堆和栈的区别

申请方式不同,栈由系统自己申请和释放,堆是自己申请和释放

申请限制大小不同,栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定,可以通过ulimit -a查看;堆向高地址扩展,是不连续的内存区域,大小可以灵活调整

申请效率不同,栈由系统分配,速度快,不会有碎片;堆由自己分配,速度慢,且会有碎片

栈空间默认是4M,堆区一般是1G-4G;栈是一块连续的内存区域,大小是操作系统预先定好的,Windows下栈大小是2M也有是1M,堆是不连续的内存区域(因为系统用链表来存储空间内存地址,自然不是连续的),堆大小手续受限于计算机系统中有效的虚拟地址

堆的内存管理机制:系统中有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆节点,删除空闲节点链表中的结点,并将该节点空间分配给程序(大多数系统会在这块内存空间首地址空间记录本次分配大小,这样delete才能正确释放本内存空间,另外系统会将多余的部分重新放入链表)

栈的内存管理机制:只要栈的剩余空间大于所申请的空间,系统为程序提供内存,否则报异常提示栈溢出

碎片问题:对于堆,频繁地new/delete会造成大量碎片,使程序效率降低;对于栈,不会产生内存区别

生长方向:堆向上,向高地址方向增长;栈向下,向低地址方向增长

分配方式:堆都是动态分配;栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配,但栈的动态分配资源由编译器进行释放,无需程序员释放

分配效率:堆是由c++函数库提供,机制复杂,所以堆的效率比栈低很多;栈是其系统提供的数据结构,计算机在底层对栈提供支持,分配专门寄存器存放栈地址,栈操作有专门指令

6.new/delete 与malloc/free的异同

new/delete是c++运算符,后者是malloc/free的C函数

new自动计算分配的空间大小,malloc需要手动计算

new是类型安全的,malloc不是:将 new 的结果赋值给不兼容的指针类型,编译器会报错,malloc它返回的是 void* 类型的指针,使用时需要显式转换为目标类型的指针,可能导致类型不安全。

new调用名为operator new的标准库函数,分配足够空间并调用相关对象的构造函数,然后调用operator delete的标准库函数释放该对象;

new是封装了malloc,直接free不会报错,但是这只是释放内存,而不会调用析构函数

7.new和delete是如何实现的

new首先调用名为operator new的标准库函数,分配足够大的内存,以保存指定类型的对象;接下来允许构造函数,最后返回对象的指针

delete首先调用operator delete的标准函数来释放对象

8.被free回收的内存是立即返回给操作系统吗

被free回收的内存首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回,这样就避免了频繁地系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片

9.宏定义和函数有何区别

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

动定义属于在结构中插入代码,没有返回值;函数调用有返回值

10.宏定义和typedef区别

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

宏替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分

宏不检查类型;typedef会检查 数据类型

宏不是语句,不在最后加分号;typedef是语句,要加分号表示结束

对指针的操作typedef char* p_char和#define p_char char*区别巨大

11.变量声明和定义区别

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

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

12.strlen和sizeof区别

sizeof是运算符,strlen是函数

sizeof参数可以任何数据的类型或者数据,strlen只能是以\0结尾的字符串

sizeof在编译的时候确定,所以不能用来得到动态分配存储空间的大小

13.struct和class的区别

struct和class都拥有成员函数和成员变量,以及权限;任何class可以完成的struct也可以

区别是struct的默认权限是public,而class的默认权限是private;在继承的时候,class的默认继承是private继承,而struct默认是public继承

在C语言中,struct是没有权限的设置的,而且也不能拥有成员函数,所以在C语言中struct是用户自定义数据类型(UDT),C++是抽象数据类型(ADT);而且在C语言中,定义结构体变量的时候是需要加上struct的,而C++不用

14.define宏定义和const的区别

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

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

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

内存占用

define只是将宏名称进行替换,在内存中产生多份相同的备份,也就是说,代码中每次使用宏时,编译器都会插入一份相同的内容,如果在多个地方使用了同一个宏,会在生成的代码中产生多份相同的代码,这可能导致代码膨胀,增加编译后的可执行文件的大小

const在程序运行中只有一份备份,且可以执行常量折叠,也就是编译器可以计算出常量表达式的值,并将其直接替换为计算结果,从而减少计算负担,能将复杂的表达式计算出结果放入常量表

宏替换发生在编译阶段之前,属于文本插入替换,const作用发生在编译过程之中

宏定义的数据没有分配内存空间,只是插入替换掉,const定义的变量只是值不能改变,但要分配内存空间

使用 const 常量更容易调试和维护,因为它们在代码中有明确的定义和类型,而宏的使用可能导致难以追踪的错误。

15.const和static的作用

static

在不考虑类的情况,所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在本文件所在的编译模块中使用;static默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存储在全局未初始化区;静态变量在函数内定义,始终存在,且只能进行一次初始化,作用范围与局部变量相同,函数退出后仍然存在,但不能使用

在考虑类的情况,static修饰成员变量,那么该变量只与类关联,不与类的对象关联。定义时需要分配空间,不能在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问;static函数不具有this指针,无法访问类对象的非static成员变量和非static的成员函数,不能声明为const常函数,虚函数和volatile;静态成员变量不会被派生类继承,派生类不能访问基类的成员变量,需要访问要加类作用域名

const

const常量在定义时必须初始化,之后无法更改;const形参可以接收const和非const类型的实参

const成员变量只能通过构造函数初始化列表进行初始化并且必须要有构造函数

const常对象不能调用非const函数,但是非const对象可以调用const对象

const成员函数不可以改变非mutable关键字修饰的变量

const修饰变量与staic一样有隐藏作用,只能在该文件中使用,其他文件不可以引用声明使用。因此在头文件中声明const变量是没有问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突

16.数组名和指针的区别

二者都可以通过偏移量来访问数组的元素

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

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

17.final和override

override是当派生类需要重写父类的虚函数时,可以指定该函数一定是重写了父类的虚函数,避免了出现自己定义一个虚函数或者重载了一个函数,这是让编译器来监视的

final关键字是不希望某个类被继承或者不希望某个虚函数被重写,可以在类名或函数名后添加final,如果做出重写行为那么编译器会报错

class B final : A;指明B是不可以被继承的

void foo()final;不可以被重写