3

变量默认初始化的几种不同情况

#include <iostream>
#include <string>

string global_str;
int global_int;
static int sglobal_x;

int main()
{
    static int slocal_x;
    int local_int;
    string local_str;
}
  • 对于string类型的变量来说,因为string类型本身接收无参数的初始化方式,所以不论变量定义在函数内还是函数外部都默认初始化为空串。
  • 对内置类型int来说,变量 global_int 定义在所有函数体之外,根据C++的规定,global_int默认初始化为0;而变量 local_int 定义在main函数内部,将不被初始化,如果程序员试图拷贝或输出为初始化的变量,将遇到一个未定义的值。
  • 对于静态局部变量全局静态变量来说都会被初始化为0;

大端与小端的概念?各自的优势是什么?

  • 大端与小端是用来描述多字节数据在内存中的存放顺序,即字节序。大端(Big Endian)是指低地址端存放高位字节,小端(Little Endian)是指低地址端存放低位字节。
  • Big Endian:符号位的判定固定为第一个字节,容易判断正负。
  • Little Endian:长度为1,2,4字节的数,排列方式都是一样的,数据类型转换非常方便。
  • 需要记住计算机是以字节为存储单位

举一个例子,比如数字0x12 34 56 78在内存中的表示形式为:

  • 1)大端模式:

低地址 -----------------> 高地址
0x12 | 0x34 | 0x56 | 0x78

  • 2)小端模式:

低地址 ------------------> 高地址
0x78 | 0x56 | 0x34 | 0x12

short int x;
char x0,x1;
x=0x1122;
x0=((char*)&x)[0]; //低地址单元
x1=((char*)&x)[1]; //高地址单元

new 和 malloc 的区别

  • new是运算符,malloc()是一个库函数
  • new会调用构造函数,malloc不会;
  • new返回指定类型指针,malloc返回void*指针;
  • new会自动计算需分配的空间,malloc不行;
  • new可以被重载,malloc不能。

指针和引用的区别

  • 指针是一个实体,而引用仅是个别名
  • 引用使用时无需解引用(*),指针需要解引用;
  • 引用只能在定义时被初始化一次,之后不可变,而指针可变;
  • 引用没有const,指针有const;
  • 引用不能为空,指针可以为空;
  • 从内存分配上看,指针变量需分配内存,引用则不需要;
  • sizeof(引用)得到所指对象的大小,sizeof(指针)得到指针本身的大小;
  • 指针和引用的自增(++)运算意义不一样。

static关键字的作用

在C语言中:

  • 加了 static 的全局变量和函数,对其他源文件隐藏(不能跨文件了)。
  • static修饰的函数内的局部变量,生存期为整个源程序运行期间,但作用域仍为函数内。
  • static变量和全部变量一样,存在静态存储区,会默认初始化为0.

在C++语言中,仍然有上面的作用,但多了下面两个:

  • 声明静态成员变量,需要在类体外使用作用域运算符进行初始化。
  • 声明静态成员函数,在函数中不能访问非静态成员变量和函数。

C++的内存分区

  • 栈区(stack):主要存放函数参数以及局部变量,由系统自动分配释放。
  • 堆区(heap):由用户通过 malloc/new 手动申请,手动释放。
  • 全局/静态区:存放全局变量、静态变量;程序结束后由系统释放。
  • 字符串常量区:字符串常量就放在这里,程序结束后由系统释放。
  • 代码区:存放程序的二进制代码。

clipboard.png

堆和栈的区别

  • 栈由编译器自动分配释放,存放函数参数、局部变量等。而堆由程序员手动分配和释放;
  • 栈是向低地址扩展的数据结构,是一块连续的内存的区域。而堆是向高地址扩展的数据结构,是不连续的内存区域;
  • 栈的默认大小为1M左右,而堆的大小可以达到几G,仅受限于计算机系统中有效的虚拟内存

clipboard.png
(小端)

vector、map、multimap、unordered_map、unordered_multimap的底层数据结构,以及几种map容器如何选择?

底层数据结构

  • vector基于数组,map、multimap基于红黑树,unordered_map、unordered_multimap基于哈希表

根据应用场景进行选择:

  • map/unordered_map 不允许重复元素
  • multimap/unordered_multimap 允许重复元素
  • map/multimap 底层基于红黑树,元素自动有序,且插入、删除效率高
  • unordered_map/unordered_multimap 底层基于哈希表,故元素无序,查找效率高。

内存泄漏怎么产生的?如何避免?

  • 内存泄漏一般是指堆内存的泄漏,也就是程序在运行过程中动态申请的内存空间不再使用后没有及时释放,导致那块内存不能被再次使用。
  • 更广义的内存泄漏还包括未对系统资源的及时释放,比如句柄、socket等没有使用相应的函数释放掉,导致系统资源的浪费。

解决方法:

  • 养成良好的编码习惯和规范,记得及时释放掉内存或系统资源。
  • 重载new和delete,以链表的形式自动管理分配的内存。
  • 使用智能指针,share_ptr、auto_ptr、weak_ptr。

说几个C++11的新特性

  • auto类型推导
  • 范围for循环
  • lambda函数
  • override 和 final 关键字
  • 空指针常量nullptr
  • 线程支持、智能指针等

static_cast 和 dynamic_cast 的区别

  • cast发生的时间不同,一个是static编译时,一个是runtime运行时
  • static_cast是相当于C的强制类型转换,用起来可能有一点危险,不提供运行时的检查来确保转换的安全性。
  • dynamic_cast用于转换指针和和引用不能用来转换对象 ——主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。在多态类型之间的转换主要使用dynamic_cast,因为类型提供了运行时信息

const限定符

  • 在定义常变量时必须同时对它初始化,此后它的值不能再改变。常变量不能出现在赋值号的左边(不为可赋值的“左值”);
  • 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
  • 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
  • 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量,如果要改变某个变量,记得将变量声明为mutable
  • 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不能被赋值。
//operator*的返回结果必须是一个const对象,否则下列代码编译出错
const classA operator*(const classA& a1,const classA& a2);  
classA a, b, c;
(a*b) = c;  
//对a*b的结果赋值。操作(a*b) = c显然不符合编程者的初衷,也没有任何意义
//因此将返回值声明为const,直接杜绝这种行为。

const与#define的区别

  • const常量有数据类型,可以寻址;而宏常量没有数据类型,不能寻址。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
  • 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
  • 在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。

sizeof运算符

  • sizeof是C语言的一种单目操作符,它并不是函数。操作数可以是一个表达式或类型名。数据类型必须用括号括住,sizeof(int);变量名可以不用括号括住。
int a[50];  //sizeof(a)=200
int *a=new int[50];  //sizeof(a)=4;
Class Test{int a; static double c};  //sizeof(Test)=4
Test *s;  //sizeof(s)=4
Class Test{ };  //sizeof(Test)=1
int func(char s[5]);  //sizeof(s)=4;
  • 数组类型,其结果是数组的总字节数;指向数组的指针,其结果是该指针的字节数。
  • 函数中的数组形参或函数类型的形参,其结果是指针的字节数。数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址.
  • 联合类型,其结果采用成员最大长度对齐。
  • 结构类型或类类型,其结果是这种类型对象的总字节数,包括任何填充在内。
  • 类中的静态成员不对结果产生影响,因为静态变量的存储位置与结构或者类的实例地址无关;
  • 没有成员变量的类的大小为1,因为必须保证类的每一个实例在内存中都有唯一的地址;
  • 有虚函数的类都会建立一张虚函数表,表中存放的是虚函数的函数指针,这个表的地址存放在类中,所以不管有几个虚函数,都只占据一个指针大小

strlen 和 sizeof 的区别

    • sizeof(...)是运算符,在头文件中typedef为unsigned int,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等。
    • 它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。
    • 由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小。实际上,用sizeof来返回类型以及静态分配的对象、结构或数组所占的空间,返回值跟对象、结构、数组所存储的内容没有关系。
    • strlen(...)是函数,要在运行时才能计算。参数必须是字符型指针(char*)。当数组名作为参数传入时,实际上数组就退化成指针了。
    • 它的功能是:返回字符串的长度。该字符串可能是自己定义的,也可能是内存中随机的,该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符/0。返回的长度大小不包括/0。
    char arr[10] = "What?";
    int len_one = strlen(arr);
    int len_two = sizeof(arr); 
    cout << len_one << " and " << len_two << endl;
     
    输出结果为:5 and 10
    sizeof返回定义arr数组时编译器为其分配的数组空间大小,不关心里面实际存储了多少数据。
    strlen只关心存储的数据内容,不关心空间的大小和类型。
    char  *parr = new char[10];
    int len_one = strlen(parr);
    int len_two = sizeof(parr);
    int len_three = sizeof(*parr);
    cout << len_one << " and " << len_two << " and " << len_three << endl;
    输出结果:24 and 4 and 1
    第一个输出结果24实际上每次运行可能不一样,这取决于parr里面存了什么(从parr[0]开始知道遇到第一个NULL结束);
    第二个结果实际上本意是想计算parr所指向的动态内存空间的大小,但是事与愿违,sizeof认为parr是个字符指针,因此返回的是该指针所占的空间(指针的存储用的是长整型,所以为4);
    第三个结果,由于*parr所代表的是parr所指的地址空间存放的字符,所以长度为1。

    结构体的内存对齐

    • 每个成员相对于这个结构体变量地址的偏移量正好是该成员类型所占字节的整数倍。为了对齐数据,可能必须在上一个数据结束和下一个数据开始的地方插入一些没有用处字节。
    • 最终占用字节数为成员类型中最大占用字节数的整数倍
    • 一般的结构体成员按照默认对齐字节数递增或是递减的顺序排放,会使总的填充字节数最少。
    struct AlignData1
    {
        char c;
        short b;
        int i;
        char d;
    }Node;
    
    这个结构体在编译以后,为了字节对齐,会被整理成这个样子:
    struct AlignData1
    {
        char c;
        char padding[1];
        short b;
        int i;
        char d;
        char padding[3];
    }Node;

    虚函数的实现原理

    • 编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。类的每个虚函数占据虚函数表中的一块,如果类中有N个虚函数,那么其虚函数表将有4N字节的大小
    • 编译器在有虚函数的类的实例中创建了一个指向这个表的指针,该指针通常存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能)。这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
    • 有虚函数或虚继承的类实例化后的对象大小至少为4字节(确切的说是一个指针的字节数;说至少是因为还要加上其他非静态数据成员,还要考虑对齐问题);没有虚函数和虚继承的类实例化后的对象大小至少为1字节(没有非静态数据成员的情况下也要有1个字节来记录它的地址)。

    哪些函数适合声明为虚函数,哪些不能?

    • 当存在类继承并且析构函数中有必须要进行的操作时(如需要释放某些资源,或执行特定的函数)析构函数需要是虚函数,否则若使用父类指针指向子类对象,在delete时只会调用父类的析构函数,而不能调用子类的析构函数,从而造成内存泄露或达不到预期结果;
    • 内联函数不能为虚函数:内联函数需要在编译阶段展开,而虚函数是运行时动态绑定的,编译时无法展开
    • 构造函数不能为虚函数:构造函数在进行调用时还不存在父类和子类的概念,父类只会调用父类的构造函数,子类调用子类的,因此不存在动态绑定的概念;但是构造函数中可以调用虚函数,不过并没有动态效果,只会调用本类中的对应函数;
    • 静态成员函数不能为虚函数:静态成员函数是以类为单位的函数,与具体对象无关,虚函数是与对象动态绑定的

    程序加载时的内存分布

    • 在多任务操作系统中,每个进程都运行在一个属于自己的虚拟内存中,而虚拟内存被分为许多页,并映射到物理内存中,被加载到物理内存中的文件才能够被执行。这里我们主要关注程序被装载后的内存布局,其可执行文件包含了代码段,数据段,BSS段,堆,栈等部分,其分布如下图所示。

    clipboard.png

    • 代码段(.text):用来存放可执行文件的机器指令。存放在只读区域,以防止被修改。
    • 只读数据段(.rodata):用来存放常量存放在只读区域,如字符串常量、全局const变量等。
    • 可读写数据段(.data):用来存放可执行文件中已初始化全局变量,即静态分配的变量和全局变量。
    • BSS段(.bss):未初始化的全局变量和局部静态变量一般放在.bss的段里,以节省内存空间。
    • :用来容纳应用程序动态分配的内存区域。当程序使用malloc或new分配内存时,得到的内存来自堆。堆通常位于栈的下方。
    • :用于维护函数调用的上下文。栈通常分配在用户空间的最高地址处分配。
    • 动态链接库映射区:如果程序调用了动态链接库,则会有这一部分。该区域是用于映射装载的动态链接库。
    • 保留区:内存中受到保护而禁止访问的内存区域。

    智能指针

    • 智能指针是在 <memory> 头文件中的std命名空间中定义的,该指针用于确保程序不存在内存和资源泄漏且是异常安全的。它们对RAII“获取资源即初始化”编程至关重要,RAII的主要原则是为将任何堆分配资源(如动态分配内存或系统对象句柄)的所有权提供给其析构函数包含用于删除或释放资源的代码以及任何相关清理代码的堆栈分配对象。大多数情况下,当初始化原始指针或资源句柄以指向实际资源时,会立即将指针传递给智能指针。
    • 智能指针的设计思想:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。
    • unique_ptr只允许基础指针的一个所有者。unique_ptr小巧高效;大小等同于一个指针且支持右值引用,从而可实现快速插入和对STL集合的检索。
    • shared_ptr采用引用计数的智能指针,主要用于要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时)的情况。当所有的shared_ptr所有者超出了范围或放弃所有权,才会删除原始指针。大小为两个指针;一个用于对象,另一个用于包含引用计数的共享控制块。最安全的分配和使用动态内存的方法是调用make_shared标准库函数,此函数在动态分配内存中分配一个对象并初始化它,返回对象的shared_ptr。

    智能指针支持的操作

    • 使用重载的->和*运算符访问对象。
    • 使用get成员函数获取原始指针,提供对原始指针的直接访问。你可以使用智能指针管理你自己的代码中的内存,还能将原始指针传递给不支持智能指针的代码。
    • 使用删除器定义自己的释放操作。
    • 使用release成员函数的作用是放弃智能指针对指针的控制权,将智能指针置空,并返回原始指针。(只支持unique_ptr)
    • 使用reset释放智能指针对对象的所有权。
    #include <iostream>
    #include <string>
    #include <memory>
    using namespace std;
    
    class base
    {
    public:
        base(int _a): a(_a)    {cout<<"构造函数"<<endl;}
        ~base()    {cout<<"析构函数"<<endl;}
        int a;
    };
    
    int main()
    {
        unique_ptr<base> up1(new base(2));
        // unique_ptr<base> up2 = up1;   //编译器提示未定义
        unique_ptr<base> up2 = move(up1);  //转移对象的所有权 
        // cout<<up1->a<<endl; //运行时错误 
        cout<<up2->a<<endl; //通过解引用运算符获取封装的原始指针 
        up2.reset(); // 显式释放内存 
    
        shared_ptr<base> sp1(new base(3));
        shared_ptr<base> sp2 = sp1;  //增加引用计数 
        cout<<"共享智能指针的数量:"<<sp2.use_count()<<endl;  //2
        sp1.reset();  //
        cout<<"共享智能指针的数量:"<<sp2.use_count()<<endl;  //1
        cout<<sp2->a<<endl; 
        auto sp3 = make_shared<base>(4);//利用make_shared函数动态分配内存 
    }
    

    燃烧你的梦
    238 声望17 粉丝