基础语法
1. 结构体内存对齐问题?
·结构体内存成员按照声明顺序存储,第一成员地址和整个结构体地址相同;
·未特殊说明时,按照结构体中size最大的成员对齐(若有double成员,按8字节对齐);
·C++11以后引入两个关键字alignas与alignof。其中alignof可以计算出类型的对齐方式,alignas可以指定结构体的对齐方式,但是alignas在某些情况下是不能使用的(若alignas小于自然对齐的最小单位,则被忽略)
·如果想使用单字节对齐的方式,使用alignas是无效的。应该使用#pragma pack(push,1)或者使用__attribute__((packed))
2. 指针和引用的区别
·指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
·指针可以有多级,引用只有一级
·指针可以为空,引用不能为null且在定义是必须初始化
·指针在初始化后可以改变指向,而引用在初始化之后不能再改变
·sizeof指针得到的是指针本身的大小,sizeof引用得到的是引用所指向变量的大小
·引用本质是一个指针,同样会占4个字节内存;指针是具体变量,需要占存储空间
·引用在声明时必须初始化为另一个变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量
·引用一旦初始化之后就不可以在改变;指针变量可以重新指向别的变量
·不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针
3. 在传递函数参数时,什么时候该使用指针,什么时候该用引用?
·需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要释放指针,不然会内存泄露。而返回局部变量的引用是没有意义的
·对栈空间的大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时对象,开销要更小
·类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式
4. 堆和栈的区别
·申请方式不同
栈由系统自动分配
堆是自己申请设释放的
·申请大小限制不同
栈顶和栈底是之前预设好的,栈是向栈底扩展,大小可以固定,可以通过ulimit -a查看,由ulimit -s修改
堆向高地址扩展,是不连续的内存区域,大小可以灵活调整
·申请效率不同
栈由系统分配,速度快,不会有碎片
堆由程序员分配,速度慢,且会有碎片
栈空间默认是4M,堆区一般是1G-4G
堆 |
栈 |
|
管理方式 |
堆区资源由程序员控制(容易产生memory leak) |
栈资源由编译器自动管理,无需手工控制 |
内存管理机制 |
系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆节点,删除空闲节点链表中的该节点,并将该节点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确的释放本内存空间,另外系统会将多余的部分重新放入空闲链表中) |
只要栈剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示溢出。(这一块理解一下链表和队列的区别,不连续空间和连续空间的区别,应该就比较好理解这两种机制的区别) |
空间大小 |
堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit系统理论上是4G),所以堆的空间比较灵活,比较大 |
栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在编译是确定,VC中可设置) |
碎片问题 |
对于堆,频繁的new/delete会造成大量碎片,使程序效率降低 |
对于栈,它是有点类似于数据结构上的一个先进后出的栈,进出一一对应,不会产生碎片。 |
生长方式 |
堆向上,向高地址方向增长 |
栈向下,向低地址方向增长 |
分配方式 |
堆都是动态分配(没有静态分配的堆) |
栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配的资源由编译器进行释放,不需要程序员实现 |
分配效率 |
堆由C/C++函数库提供,机制很复杂。所以堆的效率比栈低很多 |
栈是其系统提供的诗句结构,计算机在底层堆对栈提供支持,分配专门寄存器存放栈地址,栈操作有专门指令 |
5. 你觉得堆快还是栈快?
栈快:
因为操作系统会在底层对栈提供支持,会分配专门的寄存器放栈的地址,栈的入栈出栈也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。
而堆的操作是由C/C++函数库提供的,在分配内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问地址,第二次根据指针保存的地址访问内存,因此比较慢。
7. 区别一下指针类型?
·int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。
·int (*p)[10]表示数组指针,强调是指针,只是一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。
·int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int*类型的。
·int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型的参数,并且返回值是int类型的。
7. new/delete与malloc/free的异同
相同点
·都可用于内存的动态申请和释放
不同点
·前者是C++运算符,后者是C/C++语言标准库函数
·new自动计算要分配的空间大小,malloc需要手工计算
·new是类型安全的,malloc不是
·new调用名为operator new的标准库函数分配足够空间并调用相关对象的构造函数,delete对指针所指向对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存。后者均没有相关调用。
·后者需要库文件支持,前者不用
·new是封装了malloc,直接free不会报错,但是这只是释放内存,而不会析构对象
8. new和delete是如何让实现的?
new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个析构函数,用指定初始化对象;最后返回指向新分配并构造后的对象的指针
·delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为opertor delete的标准库函数释放该对象所用内存
9. malloc和new的区别?
·malloc和free是标准库函数,支持覆盖;new和delete是运算符,支持重载
·malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数
·malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针
10. 既然有了malloc/free,C++中为什么还需要new/delete呢?直接用malloc/free不好吗?
·malloc/free和new/delete都是用来申请内存和回收内存
·在对非基本数据类型的对象使用的时候,对象创建的时候还需要执行构造函数,销毁的时候执行析构函数。而malloc/free是库函数,是已经编译的代码,所以不能把构造函数和析构函数的功能强加给malloc/free,所以new/delete是必不可少的。
11. 被free回收的内存是立即返回还给操作系统吗?
不是的,被free回收的内存首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。
12. 宏定义和函数有何区别?
·宏在预处理阶段完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数
·宏定义属于在结构中插入代码,没有返回值;函数调用具有返回值
·宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。
·宏定义不要在最后加分号
13.变量声明和定义区别?
·声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间
·相同变量可以在多处声明(外部变量extern),但只能在一处定义
14.strlen和sizeof区别?
·sizeof是运算符,并不是函数,结果在编译时得到而非运行时获得;strlen是字符处理的库函数
·sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾时‘\0’的字符串
·因为sizeof值在编译时确定,所以不能用来得到动态分配存储空间的大小
·一个指针占内存的大小跟编译环境有关,而与机器的位数无关
15.常量指针和指针常量的区别?
·指针常量是一个指针,读常量的指针,指向一个只读变量,也就是后面所指明的int const和const int,都是一个常量,可以写作int const *p或const int *p
·常量指针是一个不能给改变指向的指针。指针是个常量,必须初始化,一旦初始化完成,它的值(也就是存放指针中的地址)就不能再改变了,即不能中途改变指向,如int *const p
16.a和&a有什么区别?
假设数组int a[10];int (*p)[10] = &a;其中:
·a是数组名,是数组首元素地址,+1表示地址值加上一个int类型的大小,如果a的值是0x00000001,加1操作后变为0x00000005。*(a+1)= a[1]
·&a是数组的指针,其类型为int (*)[10](数组指针),其加1时,系统会认为是数组首地址加上整个数组偏移(10个int型变量),值为数组a尾元素后一个元素的地址
·若(int *)p,此时输出*p时,其值为a[0]的值,因为被转为int *类型,解引用时按照int类型大小类读取。
17.C++和C语言的区别
·C语言是面向过程的,C++是面向对象的
·C++中的new和delete是对内存分配的运算符,取代了C中的malloc和free
·标准C++中的字符串类取代了标准C函数库头文件中的字符数组处理函数
·C++中的try/catch/throw异常处理机制取代了标准C中的setjmp()和ongjmp()函数
·在C++中,允许有相同的函数名,不过他们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言中是不允许的。也就是C++可以重载,C语言不允许
·C++语言中,允许变量定义语句在程序中的任何地方,只要在使用它之前就可以;而C语言中,必须要在函数开头部分。而且C++不允许重复定义变量,C语言也是做不到这一点的
·在C++中,除了值和指针之外,新增了引用。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不同,其他都是相同的
·C++相对与C增加了一些关键字,如:bool,using,dynami_cast等
18.C++中struct和class的区别
相同点
·两者都拥有成员函数、共有和私有部分
·任何可以使用class完成的工作,同样可以使用struct完成
不同点
·两者中如果不对成员指定公私有,struct默认为公有的,class默认为私有的
·class默认是private继承,而struct默认是public继承
·结构体存放在栈中,类存放在堆中
·结构体成员变量不能指定初始值,而类可以
·结构体不能 申明无参的构造函数和析构函数,而类可以
·结构体不能被继承,而类可以
19.C++和C的struct区别
·C语言中:struct是用户自定义数据类型;C++中struct是抽象数据类型,支持成员函数的定义,(C++中的struct能继承,能实现多态)
·C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可是函数
·C++中,struct增加了访问权限,且可以和类一样有成员函数,成员默认访问说明符为public(为了与C兼容)
·struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class{};);C++中结构标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例
20.define宏定义和const的区别
编译阶段
·define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用
安全性
·define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错
·const常量有数据类型,编译器可以对其进行类型安全检查
内存占用
·define只是将宏名称进行替换,在内存中会产生多分相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的表达式计算出结果放入常量表
·宏替换发生在编译阶段之前,属于文本插入替换;const作用发生于编译过程中
·宏不检查类型;const会检查数据类型
·宏定义色数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间
21.C++中const和struct的作用
struct
·不考虑类的情况
隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用
默认初始为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区
静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后任然存在,但不能使用
·考虑类的情况
static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问
static成员函数:不具有this指针,无法访问类对象的非static成员变量和static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问
const
·不考虑类的情况
const常量在定义时必须初始化,之后无法改变
const形参可以接收const和非const类型的实参,例如//i可以时int型或者const int型void fun(const int &i){//…}
·考虑类的情况
const成员变量:不能在定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化
const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字生命的变量可以在const成员函数中被修改)数据的值
const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突
22.数组名和指针区别?
·二者均可通过增减偏移量来访问数组的元素
·数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作
·当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了
23.初始化和赋值的区别
·对于简单类型来说,初始化和赋值没什么区别
·对于类和复杂数据类型来说,这两者的区别就大了,例如:
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;
}
24.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);
}
综上,总结出使用方法,在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();
25.野指针和悬空指针
都是指向无效内存区域(这里的无效区域指的是“不安全不可控”)的指针,访问行为将会导致未定义行为。
·野指针
野指针,指的是没有被初始化过的指针
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之后没有及时置空=>释放操作后立即置空。
26.C和C++的类型安全
什么是类型安全?
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。“类型安全”常被用来形容编程语言,其根据在于该门编程语言是否提供保障类型安全的机制;有的时候也用“类型安全”形容某个程序,判别的标准在于该程序是否隐含类型错误。
类型安全的编程语言与类型安全的程序之间,没有必然联系。好的程序员可以使用类型不那么安全的语言写出来类型相当安全的程序,相反的,差一点的长序员可能使用类型相当安全的语言写出类型不太安全的程序。绝对类型安全的编程语言暂时还没有。
(1)C的类型安全
C只在局部上下文中表现类型安全,比如试图从一种结构体的指针转换成另一种结构体的指针时,编译器会报错误,除非使用显式类型转换。然而,C中相当多的操作是不安全的。以下是两个十分常见的例子:
·printf格式输出
#include <stdio.h>
int mian()
{
printf(“整型输出:%d\n”,10);
printf(“浮点型输出:%f\n”,10);
return 0;
}
结果:
整型输出:10
浮点型输出:0.00000
上述代码中,使用%d控制整型数字输出,没有问题,但是改成%f时,明显输出错误,再改成%s时,运行直接报segmentation fault错误
·malloc函数的返回值
malloc是C中进行内存分配的函数,它的返回类型是void*即空类型指针,常常有这样的用法char *pStr=(char*)malloc(100*sizeof(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设计更多具体类型检查
27.C++中的重载、重写(覆盖)和隐藏的区别
(1)重载(overload)
重载是指在同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。重载和函数成员是否是虚函数无关。
(2)重写(override)
重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且:
·与基类的虚函数有相同的参数个数
·与基类的虚函数有相同的参数类型
·与基类的虚函数有相同的返回值类型