秋招总结(一)-C++归纳

STL相关:https://github.com/huihut/interview/tree/master/STL

 

1.static关键字

  • 隐藏: static的全局变量和函数会对其它源文件隐藏
  • 生命周期延长:被修饰的变量位于静态存储区, 对于静态局部变量,生命周期为整个程序而并非函数作用域
  • 修饰类成员(静态成员变量和静态成员函数),静态成员变量和静态成员函数不属于任何对象,所有类实例所共有
  • 默认初始化为0,并且只会第一次调用初始化

 

2.sizeof和strlen区别

  • sizeof是运算符,在编译时计算; strlen是库函数,运行中调用
  • sizeof参数可以是任何数据类型或数据; strlen参数只能是字符指针且结尾是'\0'的字符串
  • sizeof值是在编译时确定,所以不能用来计算动态分配存储空间的大小
    char *p = "abcdef";//字符串
    char arr1[] = "abcdef";//字符串
    char arr2[] = {'a','b','c','d','e','f'};//6个字符。没有\0
    
    sizeof(p) = 8 ; // 指针大小为8字节(64位系统)
    sizeof(arr1) = 7; //arr1为数组名,计算数组大小 + 末尾'\0'
    sizeof(arr2) = 6; //和arr1区分,该情况末尾没有'\0'

    strlen(p) = 6;
    strlen(arr1) = 6;
    strlen(arr2) = 不确定;    //没有'\0',无法确定结束

    char *p = "abcdef0\0a";//字符串
    char arr1[] = "abcdef0\0a";//字符串
    char arr2[] = {'a','b','c','d','e','f','0','\0','a'};

    sizeof(p) = 8;  //指针大小为8字节(64位系统)
    sizeof(arr1) = 10; //'\0'算一个字符,加上末尾'\0'10个字符 
    sizeof(arr2) = 9; //没有结尾'\0'
    
    strlen(p) = 7;    //strlen均计算到'\0'之前
    strlen(arr1) = 7;
    strlen(arr2) = 7;

 

3.struct和class的区别

  • 结构体的默认限定符是public;类是private。

 

4.malloc和new的区别

  • malloc和free是标准库函数;new和delete是运算符,并且支持重载。
  • malloc返回void*指针, new返回对象类型指针
  • new/delete调用构造/析构函数,malloc/free不调用 
  • 分配内存大小: malloc必须显式指定字节数, new由编译器根据类型计算得出
  • 分配失败返回值: new默认抛出异常, malloc返回NULL
  • 处理数组:malloc计算数组大小后进行分配, new使用new[]处理     (注:delete[]会调用数组中每个元素的析构函数;delete和delete[]的区别:当delete常用类型数组(int char之类)时,delete和delete[]功能一样,但对于复杂类型,比如一个结构体数组(A k[] = new A[10]),delete k只会释放首元素,delete[]就会释放每个元素)

 

5.指针和引用的区别

  • 指针保存的是所指对象的地址,引用是所指对象的别名,指针需要通过解引用间接访问,而引用是直接访问;
  • 指针是具体变量,需要占用存储空间; 引用只是别名, 不占用具体存储空间;
  • 指针可以改变地址,从而改变所指的对象,而引用必须从一而终;
  • 引用在定义的时候必须初始化,而指针则不需要;
  • 指针更灵活,用的好威力无比,用的不好处处是坑,而引用用起来则安全多了,但是比较死板。

 

6.extern关键字

  • extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义; 这是声明不是定义;

 

7.(1)define和typedef区别

  • define用于定义常量及书写复杂的内容; typedef主要用于定义类型别名
  • define作用于预处理阶段,属于文本替换; typedef是编译的一部分
  • define不检查类型; typedef检查数据类型
  • define不是语句,末尾不加分号; typedef是语句,要加分号标识结束
特别注意typedef和define在处理指针的时候的问题,如:
typedef int* pInt1;
#define pInt2 int*;

pInt1 a,b;   //等价于 int *a; int *b; 定义了两个整型指针变量
pInt2 a,b;   //等价于 int *a, b;    定义了整型指针a和整型变量b

 

    (2)define和内联函数(inline)区别

inline关键字的作用:

在 c/c++ 中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了 inline 修饰符,表示为内联函数。使用 inline 修饰带来的好处我们表面看不出来,其实,在内部调用的地方都会进行替换,这样就避免了频繁调用函数对栈内存重复开辟所带来的消耗;

 

  • 使用时,define只做简单的字符串替换(预处理阶段); 而内联函数可以进行参数类型检查(编译阶段),且具有返回值;
  • 宏定义define没有类型检查, 而内联函数有类型检查;
  • 内联函数本身是函数, 强调函数特性, 具有重载等功能;
  • 类中的成员函数是默认的内联函数, 可以访问类的成员变量, 宏不可以;

 

8.条件编译#ifdef, #else, #endif作用?

  • 可以通过加#define,并通过#ifdef来判断,将某些具体模块包括进要编译的内容。
  • 用于子程序前加#define DEBUG用于程序调试。
  • 应对硬件的设置(机器类型等)。
  • 条件编译功能if也可实现,但条件编译可以减少被编译语句,从而减少目标程序大小

 

9.常引用 const typename & refname = varname

  • 常引用可以理解为常量指针
  • 常引用下,原变量值不会被别名修改,原变量的值通过原名修改
  • 常引用通常用作只读变量别名或是形参传递

 

10.指针常量和常量指针

  • 常量指针是一个指针,指向一个只读变量;    如const int *p或者int const *p;
  • 指针常量是一个不能改变指向的指针;    如int *const p;

 

11.区别以下指针类型

  • int *p[10];         指针数组,强调数组概念; 数组内每个元素都是一个int类型指针,数组大小为10
  • int (*p)[10];      数组指针,强调指针概念; 指向一个int类型的数组,数组大小为10
  • int *p(int);         函数声明,函数名是p,参数是int类型, 返回值是int*类型的
  • int (*p)(int);      函数指针,强调是指针,该指针指向的函数具有int类型参数,返回值是int类型的
  • int (*p[10])(int*);  p是函数指针数组, 数组每个元素都是 int(*p)(int*)的函数指针,返回值为int,参数为int*; 数组大小为10;

 

12.数组名和指针的区别

  • 二者均可通过增减偏移量来访问数组中的元素
  • 数组名不是真正的指针,可以理解为常指针, 数组名没有自增自减等操作
  • sizeof(数组名)是数组的大小; sizeof(指针) = 8 (64位系统)
  • 数组名当作形参传递给函数后,就退化为一般指针, 多了自增自减操作,sizeof也不能再得到数组大小;

 

13.野指针

  • 野指针也叫空悬指针; 不是指向null的指针,而是指向垃圾内存的指针
  • 产生原因: 指针遍历未及时初始化;  指针free或delete之后没有及时置空

 

14.堆和栈的区别

  • 申请方式: 栈由系统自动分配; 堆由程序员手动分配
  • 申请大小限制: 栈顶和栈底是之前预设好的,大小固定, 通过ulimit -a查看, ulimit -s修改;    堆向高地址扩展,是不连续的内存区域,大小灵活调整;
  • 申请效率: 栈由系统分配,速度快,不会有碎片;  堆由程序员分配,速度慢,且会有碎片;

 

15.volatile关键字

  • volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
  • volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。
  • 多线程中被几个任务共享的变量需要定义为volatile类型

 

16.面向对象三大特性

  • 封装性:数据和代码捆绑在一起,避免外界干扰和不确定性访问
  • 继承性:让某种类型对象获得另一种类型对象的属性和方法
  • 多态性:同一事件表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为(重载,虚函数)

 

17.public/protected/private区别

  • public的变量和函数在类的内部外部都可以访问
  • protected的变量和函数只能在类的内部和其派生类中访问
  • private修饰的元素只能在类内访问

 

18.C++空类

  • 首先,空类大小为1字节
  • 默认函数有:构造函数/析构函数/拷贝构造函数/赋值运算符

 

19.构造函数能否为虚函数,析构函数呢?

虚析构函数作用:

  • 当派生类的对象从内存中删除时,一般先调用派生类的析构函数释放该对象中的派生类部分,再调用基类的析构函数释放该对象中的基类部分,从而能够完整的释放该对象内存
  • 当用基类指针指向一个派生类对象,即 base *bptr = new child; 此时delete bptr 来撤销bptr指向的动态内存空间时;若基类的析构函数声明为虚析构函数, 此时会先调用派生类的析构函数释放该动态空间中的派生类部分,再调用基类的析构函数释放基类部分, 从而完整的释放该堆内存, 避免内存泄漏现象
  • 如果将基类的析构函数声明为虚析构函数, 那么该基类的所有派生类的析构函数都自动称为虚析构函数

 

构造函数不能是虚函数的原因:

  • 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。
  • 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。

 

20.虚函数和纯虚函数的区别

  • 虚函数是为了实现动态编联产生的,目的是通过基类类型的指针指向不同对象时,自动调用相应的、和基类同名的函数(使用同一种调用形式,既能调用派生类又能调用基类的同名函数)。虚函数需要在基类中加上virtual修饰符修饰,因为virtual会被隐式继承,所以子类中相同函数都是虚函数。当一个成员函数被声明为虚函数之后,其派生类中同名函数自动成为虚函数,在派生类中重新定义此函数时要求函数名、返回值类型、参数个数和类型全部与基类函数相同。

  • 纯虚函数只是相当于一个接口名,但含有纯虚函数的类不能够实例化。

 

21.重载,覆盖和隐藏的区别

  • 重载:重载函数通常用来命名一组功能相似的函数
  1. 两个函数在相同的类域
  2. 两个函数的名称相同
  3. 两个函数的形参列表必须不同
  4. 注意!!! 重载函数不能根据返回值来区别,C++调用函数时是可以忽略掉返回值的,编译器无法根据返回值类型来确定调用哪个函数, 当两个函数一切都相同,只是返回值不同时,这样"重载" 会出现编译错误的
  • 覆盖:覆盖是指派生类函数覆盖基类函数
  1. 两个函数在不同的类域
  2. 两个函数的函数名、参数列表、返回值类型完全相同
  3. 基类函数必须是虚函数
  • 隐藏:隐藏是指派生类的函数屏蔽了与其同名的基类函数
  1. 两个函数在不同的类域
  2. 两个函数的函数名相同,形参列表不同
  3. 如果派生类函数与基类函数形参列表相同,但是在基类函数中没有virtual关键字,也会发生函数隐藏

 

 

22.构造函数/析构函数调用顺序

  • 基类构造函数-派生类构造函数 (首先调用基类构造函数,然后调用下一个派生类构造函数,“由底向上”依次类推)
  • 派生类析构函数-基类析构函数 (首先调用派生类析构函数,然后调用基类析构函数)
  • 如果有多个基类,先调用纵向上最上层基类构造函数,如果横向继承了多个类,调用顺序为派生表从左到右顺序。
  • 成员类对象的构造函数:如果类的变量中包含其他类(类的组合),需要在调用本类构造函数前先调用成员类对象的构造函数,调用顺序遵照在类中被声明的顺序

 

23.深拷贝和浅拷贝

  • 浅拷贝:浅拷贝仅仅是拷贝指针字面值。这是由于当我们没有自定义拷贝构造函数时,会在拷贝对象时调用默认拷贝构造函数,进行的是浅拷贝!即会出现两个指针指向同一个内存空间的问题。此时若某个对象调用析构函数释放掉指针所指向的数据,则会造成空悬指针的情况。
  • 深拷贝:当被拷贝对象存在动态分配的存储空间时,必须要自己定义拷贝构造函数,需要先动态申请一块存储空间,然后逐字节拷贝内容。使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。

 

24.什么是虚指针

  • 虚指针或虚函数指针是虚函数的实现细节。

  • 虚指针指向虚表结构。

 

25.this指针是什么?

  • this指针是类的指针,指向对象的首地址。

  • this指针只能在成员函数中使用,在全局函数、静态成员函数中都不能用this。

  • this指针只有在成员函数中才有定义,且存储位置会因编译器不同有不同存储位置。

 

26.C++字节对齐

字节对齐的作用:

  1. 各个硬件平台对存储空间的处理不尽相同,比如一些CPU访问特定的变量必须从特定的地址进行读取,所以在这种架构下就必须进行字节对齐了,要不然读取不到数据或者读取到的数据是错误的。 
  2. 会对CPU的存取效率产生影响:比如有些平台CPU从内存中偶数地址开始读取数据,如果数据起始地址正好为偶数,则1个读取周期就可以读出一个int类型的值,而如果数据其实地址为奇数,那我们就需要2个读取周期读出数据,并对高地址和低地址进行拼凑,这在读取效率上显然已经落后了很多了。

struct字节对齐规则:

  • 某个变量存放的起始位置相对于结构的起始位置的偏移量是该变量字节数的整数倍
  • 结构所占用的总字节数是结构种字节数最长的变量的字节数的整数倍

 

struct temp
{
    static int A;
    char B;
    double C;
    int D;
}a;

sizeof(a) = 1 + 7 + 8 + 4 + 4 = 24; 

注意:

#pragma pack(n)用来自定义字节对齐方式

static静态变量其存放位置与结构体或类的实例无辜眼,不对结构体大小产生任何影响;

 

union字节对齐规则:

  • unoin 的大小必须足够容纳最宽的成员
  • union 的大小需要能够被其所包含的基础成员类型的大小所整除

 

例如:

union
{
    char x[5];
    int i;
}a;
 
int main()
{
    a.x[0] = 10;
    a.x[1] = 1;
    sizeof(a) = 8; //union分配的内存必须是union中所有数据类型的整数倍,所有是1和4的倍数
    //因为char[5]占5个字节,所以union内存大小为8个字节
}

 

 

27.C++内存管理

在C++中内存分为5个区,分别是堆、栈、自由存储区、全局/静态存储区和常量存储区

  • 堆:堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。
  • 栈: :局部变量,函数参数等存储在该区,由编译器自动分配和释放。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 自由存储区:自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。也可以说new所申请的内存区域在堆上
  • 全局/静态存储区:  全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
  • 常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量(const),不允许修改。

 

还有一种说法:栈,堆,全局/静态存储区, 常量区,代码区(存放函数体的二进制代码)

 

 

28.编译的四个步骤

1.预处理阶段(.c -> .i)

  • 展开宏定义,完成字符串常量替换
  • 处理条件编译语句,通过是否具有某个宏来决定过滤掉哪些代码
  • 处理#include指令,将被包含的文件插入到该指令所在位置
  • 过滤掉所有注释语句

2.编译阶段  (.i->.s)转换为汇编语言文件 

  • 词法分析、语法分析、语义分析
  • 中间语言生成

3.汇编阶段 (.s->.o) 得到机器语言(二进制)

  • 汇编器as 将hello.s 翻译成机器语言保存在hello.o 中(二进制文本形式)。
  • 将汇编文件翻译成可重定位目标文件

4.链接阶段

各个源代码模块独立的被编译,然后将他们组装起来成为一个整体,组装的过程就是链接。被链接的各个部分本本身就是二进制文件,所以在被链接时需要将所有目标文件的代码段拼接在一起,然后将所有对符号地址的引用加以修正。

 

静态链接:

目标文件概念:

  • 可执行目标文件:可以直接在内存中执行;
  • 可重定位目标文件:可与其它可重定位目标文件在链接阶段合并,创建一个可执行目标文件;
  • 共享目标文件:这是一种特殊的可重定位目标文件,可以在运行时被动态加载进内存并链接;

 

静态链接器以一组可重定位目标文件为输入,生成一个完全链接的可执行目标文件作为输出。

链接器主要完成一下两个任务:

  • 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。
  • 重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置。

 

静态库有以下两个问题:

  • 当静态库更新时那么整个程序都要重新进行链接;
  • 对于 printf 这种标准函数库,如果每个程序都要有代码,这会极大浪费资源。

 

动态链接:

共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示,具有以下特点:

  • 在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中;
  • 动态库运行时会先检查内存中是否已经有该库的拷贝,若有则共享拷贝,否则重新加载动态库(C语言的标准库就是动态库)。静态库则是每次在编译阶段都将静态库文件打包进去,当某个库被多次引用到时,内存中会有多份副本,浪费资源。

  • 动态库另一个有点就是更新很容易,当库发生变化时,如果接口没变只需要用新的动态库替换掉就可以了。但是如果是静态库的话就需要重新被编译。

  • 不过静态库也有优点,主要就是静态库一次性完成了所有内容的绑定,运行时就不必再去考虑链接的问题了,执行效率会稍微高一些。

 

 

29.explicit关键字

  • explicit 修饰构造函数时,可以防止隐式转换和复制初始化
  • explicit 修饰转换函数时,可以防止隐式转换,但 按语境转换 除外

 

30.C++ 智能指针

智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同,shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占”所指向的对象。标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。(C++11弃用了auto_ptr)

  • shared_ptr: 
    • 最安全的分配和使用动态内存的方法就是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。  例如:shared_ptr<int> p3 = make_shared<int>(42);
    • 它采用引用计数的方法,记录当前内存资源被多少个智能指针引用,该引用计数的内存在堆上分配。每新增一个引用计数加1,当过期时引用计数减1。只有引用计数为0时,智能指针才会自动释放引用的内存资源。shared_ptr的一个最大的陷阱是循环引用,会导致堆内存无法正确释放,从而导致内存泄漏。
    • 注:shared_ptr内部两个指针,两个指针分开初始化,指向计数器部分的是一个int*指针,存储在堆上共享,是原子性的操作;指向对象的不是原子性操作; 智能指针的实现:shared_ptr实现
  • weak_ptr:
    • 为了解决循环引用(引用计数不能递减到0,以至于对象不能释放)导致的内存泄漏,引入了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,虽然不指向引用计数的共享内存,但是可以检测所管理的对象是否已经被释放,从而避免非法访问。
    • 使用weak_ptr的成员函数use_count()可以观测资源的引用计数;
    • 成员函数expired() ,该方法主要用来探测shared_ptr指针的有效性。shared_ptr一旦被释放,指针就会被置为nullptr。

循环引用问题

 

  • unique_ptr:
    • 一种对资源具有独占性的智能指针,即一个对象资源只能同时被一个unique_ptr指向。
    • 不可拷贝和复制
    • 可进行移动构造和移动复制操作
      • unique_ptr转移给另一个unique_ptr。如果需要转移所有权,可以使用std::move()函数。
      • unique_ptr<int> pInt(new int(5));
            unique_ptr<int> pInt2 = std::move(pInt);    // 转移所有权
            //cout << *pInt << endl; // 出错,pInt为空
            cout << *pInt2 << endl;
            unique_ptr<int> pInt3(std::move(pInt2));

         

31.RAII机制

RAII是Resource Acquisition Is Initialization的简称,是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源

  • 由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。

 

32.C++常用新特性

C++11新特性

 

 

33.map和unordered_map的区别

  • set/map底层实现的机制是红黑树。红黑树是一种近似于平衡的二叉查找树,默认是按升序排序的,map内部元素根据key进行排序。在红黑树上做查找、插入、删除操作的时间复杂度为O(logN)。map内部元素根据key进行排序
  • unordered_map对应哈希表,哈希表的特点就是查找效率高,时间复杂度为常数级别O(1),而额外空间复杂度则要高出许多。
  • 对于需要高效率查询的情况,使用std::unordered_map容器。而如果对内存大小比较敏感或者数据存储要求有序的话,则可以用std::map容器。如果需要内部元素自动排序,使用map,不需要排序使用unordered_map

 

 

34.C++四种强制类型转换

  • static_cast: 强制将一种数据类型转换成另一种数据类型
  1. 例: double a = 5.5;  int b = static_cast<int> a;
  2. 基本数据类型之间转换,如int到float
  3. 把空指针转换成目标类型的空指针
  4. 用于类层次结构中基类和派生类的指针或引用的转换(进行上行转换:把派生类指针或引用转成基类表示。是安全的;进行下行转换:把基类指针或引用转成派生类表示是不安全的,因为没有动态类型检查)

注:上行转换:派生类——>基类;    下行转换:基类——>派生类

 

  • const_cast:去除对象的指针引用的const修饰
  1. 例: const int a = 10; const int *p = &a; int *q = const_cast<int*>(p); (p,q去除后都不是常量指针了)
  2. 常量指针转换为非常量指针,并且仍然指向原来的对象
  3. 常量引用被转换为非常量引用,并且仍然指向原来的对象

 

  • dynamic_cast:只有在派生类之间转换时才使用dynamic_cast,提供运行时类型检查
  1. 例: 若A是基类,B是派生类: A* a = new B, B* b = dynamic_cast<B*> a;   //下行转换:基类指针->派生类指针
  2. 在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。

 

  • reinterpret_cast:通常为运算对象的位模式提供较低层次上的重新解释,不相关类型的转换
  1. 在指针之间转换,将一个类型的指针转换为另一个类型的指针,无关类型;
  2. 将指针值转换为一个整型数,但不能用于非指针类型的转换。

 

总结:

  • 基本类型转换用static_cast

  • 去指针或引用的const属性用const_cast

  • 多态类之间的类型转换用dynamic_cast

  • 不同类型的指针类型转换用reinterpreter_cast

 

35.C++成员函数在内存中的存储方式

每个对象所占用的存储空间只是该对象的数据部分(虚函数指针和虚基类指针也属于数据部分)所占用的存储空间,而不包括函数代码所占用的存储空间

 

例如以下代码:

class D  
{  
public:  
    void printA()  
    {  
        cout<<"printA"<<endl;  
    }  
    virtual void printB()  
    {  
        cout<<"printB"<<endl;  
    }  
};  
int main(void)
{
	D *d=NULL;
	d->printA();
	d->printB();
}

以上代码,输出“printA”后,程序崩溃

类中包括成员变量和成员函数。new出来的只是成员变量,成员函数始终存在,所以如果成员函数未使用任何成员变量的话,不管是不是static的,都能正常工作。需要注意的是,虽然调用不同对象的成员函数时都是执行同一段函数代码,但是执行结果一般是不相同的。不同的对象使用的是同一个函数代码段,它怎么能够分别对不同对象中的数据进行操作呢?原来C++为此专门设立了一个名为this的指针,用来指向不同的对象。

 

C++程序的内存格局通常分为四个区:全局数据区(data area),代码区(code area),栈区(stack area),堆区(heap area)(即自由存储区),常量区。全局数据区存放全局变量,静态数据和常量;所有类成员函数和非成员函数代码存放在代码区;为运行函数而分配的局部变量、函数参数、返回数据、返回地址等存放在栈区;
在类的定义时,类成员函数是被放在代码区,而类的静态成员变量在类定义时就已经在全局数据区分配了内存,因而它是属于类的。对于非静态成员变量,我们是在类的实例化过程中(构造对象)才在栈区或者堆区为其分配内存,是为每个对象生成一个拷贝,所以它是属于对象的。

静态成员函数和非静态成员函数都是在类的定义时放在内存的代码区的,因而可以说它们都是属于类的,但是类为什么只能直接调用静态类成员函数,而非静态类成员函数(即使函数没有参数)只有类对象才能调用呢?原因是类的非静态类成员函数其实都内含了一个指向类对象的指针型参数(即this指针),因而只有类对象才能调用(此时this指针有实值)
 

需要说明,不论成员函数在类内定义还是在类外定义,成员函数的代码段都用同一种方式存储。不要将成员函数的这种存储方式和inline(内联)函数的概念混淆。不要误以为用inline声明(或默认为inline)的成员函数,其代码段占用对象的存储空间,而不用inline声明的成员函数,其代码段不占用对象的存储空间。不论是否用inline声明(或默认为inline),成员函数的代码段都不占用对象的存储空间。用inline声明的作用是在调用该函数时,将函数的代码段复制插人到函数调用点,而若不用inline声明,在调用该函数时,流程转去函数代码段的入口地址,在执行完该函数代码段后,流程返回函数调用点。inline与成员函数是否占用对象的存储空间无关,它们不属于同一个问題,不应搞混。

 

C++实现单例模式

单例模式有以下特征:

  • 有一个指向唯一实例的静态指针pInstance,并且是私有的
  • 有一个公有的函数获取这个唯一的实例,并且在需要的时候创建该实例
  • 构造函数是私有的,这样就不能从别处创建该类的实例

 

  • 通过单例模式, 可以做到:
  1.  确保一个类只有一个实例被建立 
  2. 提供了一个对对象的全局访问指针 
  3.  在不影响单例类的客户端的情况下允许将来有多个实例

 

1.懒汉式单例模式在类加载时不初始化、直到用到才会初始化、以时间换取空间模式

class Singleton {
private:
    Singleton() {}
    ~Singleton() {}
    static Singleton* pInstance;
public:
    static Singleton *GetInstance() {
        if(pInstance == nullptr)
            pInstance = new Singleton();
        return pInstace;
    }

    static Singleton *Destroy() {
        delete pInstance;
        pInstance = nullptr;
    }
};
    

 

线程安全的懒汉式单例的实现:

class Singleton {
private:
    Singleton() {}
    ~Singleton() {}
    static Singleton* pInstance;
public:
    static Singleton *GetInstance() {
        if(pInstance == nullptr) {
            Lock();
            if(pInstance == nullptr)
                pInstance = new Singleton();
            UnLock();
        }
        return pInstace;
    }

    static Singleton *Destroy() {
        delete pInstance;
        pInstance = nullptr;
    }
};
    

 

2.饿汉式单例模式: 在类加载时就完成了初始化,所以类加载比较慢、获取对象的速度快、以空间换取时间模式、线程安全

class Singleton {
private:
    Singleton() {}
    ~Singleton() {}
public:
    static Singleton* GetInstance() {
        static Singleton* pInstance;
        return &pInstance;
    }
};

 

 

 

©️2020 CSDN 皮肤主题: 像素格子 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值