C++面试常见题

windows c/c++ 专栏收录该内容
272 篇文章 4 订阅

1.什么是虚函数?什么是纯虚函数?

虚函数是允许被其子类重新定义的成员函数。

虚函数的声明:virtual returntype func(parameter);引入虚函数的目的是为了动态绑定;

纯虚函数声明:virtual returntype func(parameter)=0;引入纯虚函数是为了派生接口。(使派生类仅仅只是继承函数的接口)

2.基类为什么需要虚析构函数?

防止内存泄漏。想去借助父类指针去销毁子类对象的时候,不能去销毁子类对象。假如没有虚析构函数,释放一个由基类指针指向的派生类对象时,不会触发动态绑定,则只会调用基类的析构函数,不会调用派生类的。派生类中申请的空间则得不到释放导致内存泄漏。

3.当i是一个整数的时候i++和++i那个更快?它们的区别是什么?

几乎一样。i++返回的是i的值,++i返回的是i+1的值,即++i是一个确定的值,是一个可以修改的左值。

4.vector的reserve和capacity的区别?

reserve()用于让容器预留空间,避免再次分配内存;capacity()返回在重新进行分配以前所能容纳的元素数量。

5.如何初始化const和static数据成员?

通常在类外申明static成员,但是static const的整型(bool,char,int,long)可以在类中声明且初始化,static const的其他类型必须在类外初始化(包括整型数组)。

6.static 和const分别怎么用,类里面static和const可以同时修饰成员函数吗?

static的作用:

对变量:

1.局部变量:

在局部变量之前加上关键字static,局部变量就被定义成为一个局部静态变量。

  1)内存中的位置:静态存储区

  2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)

  3)作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束。

 注:当static用来修饰局部变量的时候,它就改变了局部变量的存储位置(从原来的栈中存放改为静态存储区)及其生命周期(局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对他进行访问),但未改变其作用域。

2.全局变量

在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量。

 1)内存中的位置:静态存储区(静态存储区在整个程序运行期间都存在)

 2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)

 3)作用域:全局静态变量在声明他的文件之外是不可见的。准确地讲从定义之处开始到文件结尾。

注:static修饰全局变量,并未改变其存储位置及生命周期,而是改变了其作用域,使当前文件外的源文件无法访问该变量,好处如下:(1)不会被其他文件所访问,修改(2)其他文件中可以使用相同名字的变量,不会发生冲突。对全局函数也是有隐藏作用。而普通全局变量只要定义了,任何地方都能使用,使用前需要声明所有的.c文件,只能定义一次普通全局变量,但是可以声明多次(外部链接)。注意:全局变量的作用域是全局范围,但是在某个文件中使用时,必须先声明。

 对类中的:

 1.成员变量

   用static修饰类的数据成员实际使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象。因此,static成员必须在类外进行初始化(初始化格式: int base::var=10;),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化 。因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。

        特点:

         1.不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef #define #endif或者#pragma once也不行。 

         2.静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。

         3.静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为 所属类类型的指针或引用。

    2.成员函数

  1. 用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针。
  2. 静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。base::func(5,3);当static成员函数在类外定义时不需要加static修饰符。
  3. 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数不含this指针。 

不可以同时用const和static修饰成员函数。

C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。

我们也可以这样理解:两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。

const的作用:

1.限定变量为不可修改。

2.限定成员函数不可以修改任何数据成员。

3.const与指针:

const char *p 表示 指向的内容不能改变。

char * const p,就是将P声明为常指针,它的地址不能改变,是固定的,但是它的内容可以改变。

7.指针和引用的区别

本质上的区别是,指针是一个新的变量,只是这个变量存储的是另一个变量的地址,我们通过访问这个地址来修改变量。

而引用只是一个别名,还是变量本身。对引用进行的任何操作就是对变量本身进行操作,因此以达到修改变量的目的。

注:

(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:
int a=1;int *p=&a;
int a=1;int &b=a;
上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。

而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。

(2)可以有const指针,但是没有const引用(const引用可读不可改,与绑定对象是否为const无关)

注:引用可以指向常量,也可以指向变量。例如int &a=b,使引用a指向变量b。而为了让引用指向常量,必须使用常量引用,如const int &a=1; 它代表的是引用a指向一个const int型,这个int型的值不能被改变,而不是引用a的指向不能被改变,因为引用的指向本来就是不可变的,无需加const声明。即指针存在常量指针int const *p和指针常量int *const p,而引用只存在常量引用int const &a,不存在引用常量int& const a。

(3)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

(4)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

(5)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

(6)"sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小;

(7)指针和引用的自增(++)运算意义不一样;

(8)指针使用时需要解引用(*),引用则不需要;

8.什么是多态?多态有什么用途?

C++ 多态有两种:静态多态(早绑定)、动态多态(晚绑定)。静态多态是通过函数重载实现的;动态多态是通过虚函数实现的。

1.定义:“一个接口,多种方法”,程序在运行时才决定要调用的函数。

2.实现:C++多态性主要是通过虚函数实现的,虚函数允许子类重写override(注意和overload的区别,overload是重载,是允许同名函数的表现,这些函数参数列表/类型不同)。

注:多态与非多态的实质区别就是函数地址是静态绑定还是动态绑定。如果函数的调用在编译器编译期间就可以确定函数的调用地址,并产生代码,说明地址是静态绑定的;如果函数调用的地址是 需要在运行期间才确定,属于动态绑定。

3.目的:接口重用。封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。

4.用法:声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。

用一句话概括:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

关于重载、重写、隐藏的区别

Overload(重载):在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。

Override(覆盖或重写):是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
注:重写基类虚函数的时候,会自动转换这个函数为virtual函数,不管有没有加virtual,因此重写的时候不加virtual也是可以的,不过为了易读性,还是加上比较好。

Overwrite(重写):隐藏,是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

虚函数表:

详细解释可以参考博客:https://www.cnblogs.com/jin521/p/5602190.html

多态是由虚函数实现的,而虚函数主要是通过虚函数表(V-Table)来实现的。

如果一个类中包含虚函数(virtual修饰的函数),那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。如下图:

这个类的每一个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),这个虚指针指向虚函数表。

注:对象不包含虚函数表,只有虚指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表。

 

  • 原始基类的虚函数表

          下图是原始基类的对象,可以看到虚指针在地址的最前面,指向基类的虚函数表(假设基类定义了3个虚函数)

           

  • 单继承时的虚函数(无重写基类虚函数)

     假设现在派生类继承基类,并且重新定义了3个虚函数,派生类会自己产生一个兼容基类虚函数表的属于自己的虚函数表

     

    Derive Class继承了Base Class中的3个虚函数,准确说是该函数的实体地址被拷贝到Derive Class的虚函数列表中,派生新增的虚函数置于虚函数列表后面,并按声明顺序摆放。

  • 单继承时的虚函数(重写基类虚函数)

     现在派生类重写基类的x函数,可以看到这个派生类构建自己的虚函数表的时候,修改了base::x()这一项,指向了自己的虚函数。

      

  • 多重继承时的虚函数(class Derived :public Base1,public Base2)

     这个派生类多重继承了两个基类base1,base2,因此它有两个虚函数表。

     

    它的对象会有多个虚指针(据说和编译器相关),指向不同的虚函数表。

          注:有关以上虚函数表等详见c++对象模型。链接地址:https://www.cnblogs.com/inception6-lxc/p/9273918.html

 

纯虚函数:

  定义: 在很多情况下,基类本身生成对象是不合情理的。为了解决这个问题,方便使用类的多态性,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;)纯虚函数不能再在基类中实现,编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。称带有纯虚函数的类为抽象类。

  特点:

  1,当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;(避免类被实例化且在编译时候被发现,可以采用此方法)

  2,这个方法必须在派生类(derived class)中被实现;

  目的:使派生类仅仅只是继承函数的接口。

9.vector中size()和capacity()的区别。

size()指容器当前拥有的元素个数(对应的resize(size_type)会在容器尾添加或删除一些元素,来调整容器中实际的内容,使容器达到指定的大小。);capacity()指容器在必须分配存储空间之前可以存储的元素总数。

size表示的这个vector里容纳了多少个元素,capacity表示vector能够容纳多少元素,它们的不同是在于vector的size是2倍增长的。如果vector的大小不够了,比如现在的capacity是4,插入到第五个元素的时候,发现不够了,此时会给他重新分配8个空间,把原来的数据及新的数据复制到这个新分配的空间里。(会有迭代器失效的问题)

10.new和malloc的区别。

       详细参考:链接

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

11.C++的内存分区

  • 栈区(stack):主要存放函数参数以及局部变量,由系统自动分配释放。
  • 堆区(heap):由用户通过 malloc/new 手动申请,手动释放。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
  • 全局/静态区:存放全局变量、静态变量;程序结束后由系统释放。
  • 字符串常量区:字符串常量就放在这里,程序结束后由系统释放。
  • 代码区:存放程序的二进制代码。

C++内存管理详细链接

12.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 底层基于哈希表,故元素无序,查找效率高。

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

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

VS下检测内存泄漏方法:

#define CRTDBG_MAP_ALLOC    
#include <stdlib.h>    
#include <crtdbg.h>    
//在入口函数中包含 _CrtDumpMemoryLeaks();    
//即可检测到内存泄露  

//以如下测试函数为例:  
int main()
{
    char* pChars = new char[10];
    //delete[]pChars;
    _CrtDumpMemoryLeaks();
    system("pause");
    return 0;
}

 

解决方法:

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

14.说几个C++11的新特性

  • auto类型推导
  • 范围for循环
  • lambda函数
  • override 和 final 关键字
  • /*如果不使用override,当你手一抖,将foo()写成了f00()会怎么样呢?结果是编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。如果这个虚函数很重要的话,那就会对整个程序不利。
    
      所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的:*/
    class A
    {
        virtual void foo();
    }
    class B :public A
    {
        void foo(); //OK
        virtual foo(); // OK
        void foo() override; //OK
    }
    
    class A
    {
        virtual void foo();
    };
    class B :A
    {
        virtual void f00(); //OK
        virtual void f0o()override; //Error 
    };

     

    /*当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。例子如下:*/
    
    class Base
    {
        virtual void foo();
    };
     
    class A : Base
    {
        void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
        void bar() final; // Error: 父类中没有 bar虚函数可以被重写或final
    };
    
    class B final : A // 指明B是不可以被继承的
    {
        void foo() override; // Error: 在A中已经被final了
    };
     
    class C : B // Error: B is final
    {
    };

     

  • 空指针常量nullptr
  • 线程支持、智能指针等

15.C和C++区别?

C++在C的基础上增添类,C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制),而对于C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。

16.const与#define的区别

1.编译器处理方式 
define – 在预处理阶段进行替换 
const – 在编译时确定其值

2.类型检查 
define – 无类型,不进行类型安全检查,可能会产生意想不到的错误 
const – 有数据类型,编译时会进行类型检查

3.内存空间 
define – 不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大 
const – 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝

4.其他 
在编译时, 编译器通常不为const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。 
宏替换只作替换,不做计算,不做表达式求解。

17.悬空指针与野指针区别

  • 悬空指针:当所指向的对象被释放或者收回,但是没有让指针指向NULL;
{
   char *dp = NULL;
   {
       char c;
       dp = &c;
   } 
  //变量c释放,dp变成空悬指针
}

 

void func()
{
    char *dp = (char *)malloc(A_CONST);
    free(dp);         //dp变成一个空悬指针
    dp = NULL;        //dp不再是空悬指针
    /* ... */
}

 

  • 野指针:那些未初始化的指针;
int func()
{
    char *dp;//野指针,没有初始化
    static char *sdp;//非野指针,因为静态变量会默认初始化为0
}

18.struct与class的区别?

本质区别是访问的默认控制:默认的继承访问权限,class是private,struct是public;

19.sizeof和strlen的区别?

功能不同:

sizeof是操作符,参数为任意类型,主要计算类型占用内存大小。

strlen()是函数,其函数原型为:extern unsigned int strlen(char *s);其参数为char*,strlen只能计算以"\0"结尾字符串的长度,计算结果不包括"\0"。

char* ss="0123456789";  
//s1=4,ss为字符指针在内存中占用4个字节  
int s1=sizeof(ss);  
//s2=10,计算字符串ss的长度  
int s2=strlen(ss);  

参数不同:

当将字符数组作为sizeof()的参数时,计算字符数组占用内存大小;当将字符数组作为strlen()函数,字符数组转化为char*。因为sizeof的参数为任意类型,而strlen()函数参数只能为char*,当参数不是char*必须转换为char*。

 

char str[]="abced";  
     //a为6(1*6),字符数组str包含6个元素(a,b,c,d,e,\0),每个元素占用1个字节  
    int a= sizeof(str);  
    //len为5,不包含"\0",  
    int len=strlen(str);  
    //str[0]是字符元素a,所以b=1  
    int b= sizeof(str[0]);  

 

20.32位,64位系统中,各种常用内置数据类型占用的字节数?

char :1个字节(固定)

*(即指针变量): 4个字节(32位机的寻址空间是4个字节。同理64位编译器)(变化*)

short int : 2个字节(固定)

int: 4个字节(固定)

unsigned int : 4个字节(固定)

float: 4个字节(固定)

double: 8个字节(固定)

long: 4个字节

unsigned long: 4个字节(变化*,其实就是寻址控件的地址长度数值)

long long: 8个字节(固定)


64位操作系统

char :1个字节(固定)

*(即指针变量): 8个字节

short int : 2个字节(固定)

int: 4个字节(固定)

unsigned int : 4个字节(固定)

float: 4个字节(固定)

double: 8个字节(固定)

long: 8个字节

unsigned long: 8个字节(变化*其实就是寻址控件的地址长度数值)

long long: 8个字节(固定)

除*与long 不同其余均相同。

21.virtual, inline, decltype,volatile,static, const关键字的作用?使用场景?

inline:在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。

 

#include <stdio.h>
//函数定义为inline即:内联函数
inline char* dbtest(int a) {
    return (i % 2 > 0) ? "奇" : "偶";
} 

int main()
{
   int i = 0;
   for (i=1; i < 100; i++) {
       printf("i:%d    奇偶性:%s /n", i, dbtest(i));    
   }
}//在for循环的每个dbtest(i)的地方替换成了 (i % 2 > 0) ? "奇" : "偶",避免了频繁调用函数,对栈内存的消耗

 

decltype:从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的的值类型。

volatile:volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

static:

  1. 隐藏

      在变量和函数名前面如果未加static,则它们是全局可见的。加了static,就会对其它源文件隐藏,利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲  突。static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏 。

    2.static变量中的记忆功能和全局生存期

      存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。PS:如果作为static局部变量在函数内定义,它的生存期为整个源程序,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。

 

#include <stdio.h>

int fun(){
    static int count = 10;       //在第一次进入这个函数的时候,变量a被初始化为10!并接着自减1,以后每次进入该函数,a
    return count--;              //就不会被再次初始化了,仅进行自减1的操作;在static发明前,要达到同样的功能,则只能使用全局变量:    

}

int count = 1;

int main(void)
{
     printf("global\t\tlocal static\n");
     for(; count <= 10; ++count)
               printf("%d\t\t%d\n", count, fun());
     return 0;
}

 

---基于以上两点可以得出一个结论:把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域, 限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。

   3.static的第三个作用是默认初始化为0(static变量)

最后对static的三条作用做一句话总结。首先static的最主要功能是隐藏,其次因为static变量存放在静态存储区,所以它具备持久性和默认值0。

  4.static的第四个作用:C++中的类成员声明static(有些地方与以上作用重叠)

 在类中声明static变量或者函数时,初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员,这样就出现以下作用:

(1)类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致 了它仅能访问类的静态数据和静态成员函数。      

(2)不能将静态成员函数定义为虚函数。      

(3)由于静态成员声明于类中,操作于其外,所以对其取地址操作,就多少有些特殊 ,变量地址是指向其数据类型的指针 ,函数地址类型是一个“nonmember函数指针”。

(4)由于静态成员函数没有this指针,所以就差不多等同于nonmember函数,结果就 产生了一个意想不到的好处:成为一个callback函数,使得我们得以将C++和C-based X W indow系统结合,同时也成功的应用于线程函数身上。 (这条没遇见过)  

(5)static并没有增加程序的时空开销,相反她还缩短了子类对父类静态成员的访问 时间,节省了子类的内存空间。      

(6)静态数据成员在<定义或说明>时前面加关键字static。      

(7)静态数据成员是静态存储的,所以必须对它进行初始化。 (程序员手动初始化,否则编译时一般不会报错,但是在Link时会报错误)     

(8)静态成员初始化与一般数据成员初始化不同:

初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆;
初始化时不加该成员的访问权限控制符private,public等;        
初始化时使用作用域运算符来标明它所属类;
           所以我们得出静态数据成员初始化的格式:
<数据类型><类名>::<静态数据成员名>=<值>

(9)为了防止父类的影响,可以在子类定义一个与父类相同的静态变量,以屏蔽父类的影响。这里有一点需要注意:我们说静态成员为父类和子类共享,但我们有重复定义了静态成员,这会不会引起错误呢?不会,我们的编译器采用了一种绝妙的手法:name-mangling 用以生成唯一的标志

22.深拷贝与浅拷贝的区别?

1.什么时候用到拷贝函数?

  a.一个对象以值传递的方式传入函数体; 
  b.一个对象以值传递的方式从函数返回;

  c.一个对象需要通过另外一个对象进行初始化。

  如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝;

2.是否应该自定义拷贝函数?

 自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。

3.什么叫深拷贝?什么是浅拷贝?两者异同?

  如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

4.深拷贝好还是浅拷贝好?

如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

参考博客:https://blog.csdn.net/caoshangpa/article/details/79226270

                  http://www.cnblogs.com/BlueTzar/articles/1223313.html

23.派生类中构造函数,析构函数调用顺序?

构造函数:“先基后派”;析构函数:“先派后基”。

24.C++类中数据成员初始化顺序?

1.成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。

2.如果不使用初始化列表初始化,在构造函数内初始化时,此时与成员变量在构造函数中的位置有关。

3.类中const成员常量必须在构造函数初始化列表中初始化。

4.类中static成员变量,只能在类内外初始化(同一类的所有实例共享静态成员变量)。

初始化顺序:

  • 1) 基类的静态变量或全局变量
  • 2) 派生类的静态变量或全局变量
  • 3) 基类的成员变量
  • 4) 派生类的成员变量

25.结构体内存对齐问题?结构体/类大小的计算?

注:内存对齐是看类型,而不是看总的字节数。比如:

 

#include<iostream>
using namespace std;

struct AlignData1
{
    int a;
    char b[7];//a后面并不会补上3个字节,而是由于char的类型所以不用补。
    short c;
    char d;
}Node;
struct AlignData2
{
    bool a;
    int b[2];//a后面并不会补上7个字节,而是根据int的类型补3个字节。
    int c;
    int d;
}Node2;
int main(){
    cout << sizeof(Node) << endl;//16
    cout << sizeof(Node2) << endl;//20
    system("pause");
    return 0;
}

 

链接

补充:

  • 每个成员相对于这个结构体变量地址的偏移量正好是该成员类型所占字节的整数倍。为了对齐数据,可能必须在上一个数据结束和下一个数据开始的地方插入一些没有用处字节。
  • 最终占用字节数为成员类型中最大占用字节数的整数倍
  • 一般的结构体成员按照默认对齐字节数递增或是递减的顺序排放,会使总的填充字节数最少。

 

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;

 

 含有虚函数的类的大小:链接

补充:联合体的大小计算:

联合体所占的空间不仅取决于最宽成员,还跟所有成员有关系,即其大小必须满足两个条件:1)大小足够容纳最宽的成员;2)大小能被其包含的所有基本数据类型的大小所整除。

 

union U1  
{  
    int n;  
    char s[11];  
    double d;  
};  //16,char s[11]按照char=1可以整除
  
union U2  
{  
    int n;  
    char s[5];  
    double d;  
};  //8
  

 

26.static_cast, dynamic_cast, const_cast, reinpreter_cast的区别? 

链接

 补充:static_cast与dynamic_cast

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

class CDerived : public CBasic 
{ 
public: 
     virtual int test(){ return 1;} 
}; 
  
int main() 
{ 
     CBasic        cBasic; 
     CDerived    cDerived; 
     CBasic * pB1 = new CBasic; 
     CBasic * pB2 = new CDerived;
     CBasic * pB3 = new CBasic;
     CBasic * pB4 = new CDerived;
     
     
     //dynamic cast failed, so pD1 is null. 
     CDerived * pD1 = dynamic_cast<CDerived * > (pB1);    
                  
     //dynamic cast succeeded, so pD2 points to  CDerived object                                         
     CDerived * pD2 = dynamic_cast<CDerived * > (pB2);
     //pD3将是一个指向该CBasic类型对象的指针,对它进行CDerive类型的操作将是不安全的
     CDerived * pD3 = static_cast<CDerived * > (pB3);     
     //static_cast成功
     CDerived * pD4 = static_cast<CDerived * > (pB4);      
      
     //dynamci cast failed, so throw an exception.             
//    CDerived & rD1 = dynamic_cast<CDerived &> (*pB1);    
  
//dynamic cast succeeded, so rD2 references to CDerived object. 
     CDerived & rD2 = dynamic_cast<CDerived &> (*pB2);    
  
     return 0; 
} 

注:CBasic要有虚函数,否则会编译出错;static_cast则没有这个限制。

27.智能指针

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

链接

28.计算类大小例子

class A {};: sizeof(A) = 1;
class A { virtual Fun(){} };: sizeof(A) = 4(32位机器)/8(64位机器);
class A { static int a; };: sizeof(A) = 1;
class A { int a; };: sizeof(A) = 4;
class A { static int a; int b; };: sizeof(A) = 4;

类中用static声明的成员变量不计算入类的大小中,因为static data不是实例的一部分。static的属于全局的,他不会占用类的存储,他有专门的地方存储 (全局变量区)

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

  • 大端与小端是用来描述多字节数据在内存中的存放顺序,即字节序。大端(Big Endian)指低地址端存放高位字节,小端(Little Endian)是指低地址端存放低位字节。
  • 需要记住计算机是以字节为存储单位
  • 为了方便记忆可把大端和小端称作高尾端和低尾端,eg:如果是高尾端模式一个字符串“11223344”把尾部“44”放在地址的高位,如果是地尾端模式,把“44”放在地址的低位。

各自优势:

  • Big Endian:符号位的判定固定为第一个字节,容易判断正负。
  • Little Endian:长度为1,2,4字节的数,排列方式都是一样的,数据类型转换非常方便。

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

  • 1)大端模式:

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

  • 2)小端模式:

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

30.C++中*和&同时使用是什么意思?

template <class T>
void InsertFront(Node<T>* & head, T item)
上面一个函数的声明,其中第一个参数*和&分别是什么意思?
head是个指针,前面为什么加个&

本来“* head”代表的是传指针的,但是只能改变head指向的内容,而“* &head”意思是说head是传进来的指针的同名指针,就能既改变*head指向的内容,又能改变head这个指针。比如:main()有个Node<int>* p,int t;当调用insertFront(p,t)是,如果template <class T> void InsertFront(Node<T>* & head, T item)中有对head进行赋值改变时,main()中的p也会跟着改变,如果没有&这个别名标识时,p则不会随着head的改变而改变。

31.C++vector与list区别

https://www.cnblogs.com/shijingjing07/p/5587719.html

32.C语言中static关键字作用

C语言中static的作用如下

第一、在修饰变量的时候,static修饰的静态局部变量只执行一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。 
第二、static修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是extern外部声明也不可以。 
第三、static修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。Static修饰的局部变量存放在全局数据区的静态变量区。初始化的时候自动初始化为0; 
(1)不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用static修饰 
(2)考虑到数据安全性(当程想要使用全局变量的时候应该先考虑使用static)


在C++中static关键字除了具有C中的作用还有在类中的使用 
在类中,static可以用来修饰静态数据成员和静态成员方法 
静态数据成员 
(1)静态数据成员可以实现多个对象之间的数据共享,它是类的所有对象的共享成员,它在内存中只占一份空间,如果改变它的值,则各对象中这个数据成员的值都被改变。 
(2)静态数据成员是在程序开始运行时被分配空间,到程序结束之后才释放,只要类中指定了静态数据成员,即使不定义对象,也会为静态数据成员分配空间。 
(3)静态数据成员可以被初始化,但是只能在类体外进行初始化,若为对静态数据成员赋初值,则编译器会自动为其初始化为0 
(4)静态数据成员既可以通过对象名引用,也可以通过类名引用。

静态成员函数 
(1)静态成员函数和静态数据成员一样,他们都属于类的静态成员,而不是对象成员。 
(2)非静态成员函数有this指针,而静态成员函数没有this指针。 
(3)静态成员函数主要用来访问静态数据成员而不能访问非静态成员。

33.C/C++中堆和栈的区别

讲解全面的一篇博客:https://blog.csdn.net/Fiorna0314/article/details/49757195

链接1

链接2

34.定义一个空类编译器做了哪些操作?

如果你只是声明一个空类,不做任何事情的话,编译器会自动为你生成一个默认构造函数、一个拷贝默认构造函数、一个默认拷贝赋值操作符和一个默认析构函数。这些函数只有在第一次被调用时,才会被编译器创建。所有这些函数都是inline和public的。

定义一个空类例如:

class Empty
{
}

一个空的class在C++编译器处理过后就不再为空,编译器会自动地为我们声明一些member function,一般编译过就相当于:

class Empty
{
public:
Empty(); // 缺省构造函数//
Empty( const Empty& ); // 拷贝构造函数//
~Empty(); // 析构函数//
Empty& operator=( const Empty& ); // 赋值运算符//
};

需要注意的是,只有当你需要用到这些函数的时候,编译器才会去定义它们。

35.友元函数和友元类

链接

36.什么情况下,类的析构函数应该声明为虚函数?为什么?

基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。

如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。

37.哪些函数不能成为虚函数?

不能被继承的函数和不能被重写的函数。

1)普通函数

普通函数不属于成员函数,是不能被继承的。普通函数只能被重载,不能被重写,因此声明为虚函数没有意义。因为编译器会在编译时绑定函数。

而多态体现在运行时绑定。通常通过基类指针指向子类对象实现多态。

2)友元函数

友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。

3)构造函数

首先说下什么是构造函数,构造函数是用来初始化对象的。假如子类可以继承基类构造函数,那么子类对象的构造将使用基类的构造函数,而基类构造函数并不知道子类的有什么成员,显然是不符合语义的。从另外一个角度来讲,多态是通过基类指针指向子类对象来实现多态的,在对象构造之前并没有对象产生,因此无法使用多态特性,这是矛盾的。因此构造函数不允许继承。

4)内联成员函数

我们需要知道内联函数就是为了在代码中直接展开,减少函数调用花费的代价。也就是说内联函数是在编译时展开的。而虚函数是为了实现多态,是在运行时绑定的。因此显然内联函数和多态的特性相违背。

5)静态成员函数

首先静态成员函数理论是可继承的。但是静态成员函数是编译时确定的,无法动态绑定,不支持多态,因此不能被重写,也就不能被声明为虚函数。、

38.编写一个有构造函数,析构函数,赋值函数,和拷贝构造函数的String类

​
//.h

class String{
    public:
        String(const char* str);
        String(const String &other);
        ~String();
        String & operator=(const String &other);
    private:
         char* m_data;
};

 

//.cpp
String::String(const char*str){
    if(str==NULL){
        m_data=new char[1];
        *m_data='\0';
    }
    else{
        int length=strlen(str);
        m_data=new char[length+1];
        strcpy(m_data,str);
    }
}

String::String(const String &other){
    int length=strlen(other.m_data);
    m_data=new char[length+1];
    strcpy(m_data,other.m_data);
}

String::~String(){
    delete [] m_data;
}

String::String& operator=(const String & other){
    if(&other==this)return *this;//检查自赋值
    delete[]m_data;//释放原有的内存资源
    int length=strlen(other.m_data);
    m_data=new char[length+1];
    strcpy(m_data,other.m_data);
    return *this;//返回本对象的引用
}

 

注:一个单链表的简单实现:链接 

39.this指针的理解

链接

40.程序加载时的内存分布

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

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

41.智能指针

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

参考:https://www.cnblogs.com/wxquare/p/4759020.html

1.引用计数问题

  • 每个shared_ptr所指向的对象都有一个引用计数,它记录了有多少个shared_ptr指向自己
  • shared_ptr的析构函数:递减它所指向的对象的引用计数,如果引用计数变为0,就会销毁对象并释放相应的内存
  • 引用计数的变化:决定权在shared_ptr,而与对象本身无关

参考:https://www.cnblogs.com/xzxl/p/7852597.html

  2.智能指针支持的操作

  • 使用重载的->和*运算符访问对象。
  • 使用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函数动态分配内存 
}

 

   3.智能指针的陷阱(循环引用等问题)

        链接

class B;
class A
{
public:
  shared_ptr<B> m_b;
};
class B
{
public:
  shared_ptr<A> m_a;
};

int main()
{
  {
    shared_ptr<A> a(new A);  //new出来的A的引用计数此时为1
    shared_ptr<B> b(new B);  //new出来的B的引用计数此时为1
    a->m_b = b;              //B的引用计数增加为2
    b->m_a = a;              //A的引用计数增加为2
  }
  //b先出作用域,B的引用计数减少为1,不为0;
  //所以堆上的B空间没有被释放,且B持有的A也没有机会被析构,A的引用计数也完全没减少

  //a后出作用域,同理A的引用计数减少为1,不为0,所以堆上A的空间也没有被释放
}

循环引用”简单来说就是:两个对象互相使用一个shared_ptr成员变量指向对方会造成循环引用。

    即A内部有指向B,B内部有指向A,这样对于A,B必定是在A析构后B才析构,对于B,A必定是在B析构后才析构A,这就是循环引用问题,违反常规,导致内存泄露。

   解决循环引用方法:

   1. 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
   2. 当A的生存期超过B的生存期的时候,B改为使用一个普通指针指向A。
   3. 使用weak_ptr打破这种循环引用,因为weak_ptr不会修改计数器的大小,所以就不会产生两个对象互相使用一个shared_ptr成员变量指向对方的问题,从而不会引起引用循环。

42.vector扩容原理说明

  • 新增元素:Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素;
  • 对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了 ;
  • 初始时刻vector的capacity为0,塞入第一个元素后capacity增加为1;
  • 不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容。
  1. vector在push_back以成倍增长可以在均摊后达到O(1)的事件复杂度,相对于增长指定大小的O(n)时间复杂度更好。
  2. 为了防止申请内存的浪费,现在使用较多的有2倍与1.5倍的增长方式,而1.5倍的增长方式可以更好的实现对内存的重复利用,因为更好。

参考链接:https://blog.csdn.net/yangshiziping/article/details/52550291

43.内联函数和宏定义的区别

1.宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率。

   内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。如果内联函数的函数体过大,编译器会自动      的把这个内联函数变成普通函数。

 

2. 宏定义是在预处理的时候把所有的宏名用宏体来替换,简单的说就是字符串替换

    内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率

 

3. 宏定义是没有类型检查的,无论对还是错都是直接替换

    内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等

 

4. 宏定义和内联函数使用的时候都是进行代码展开。不同的是宏定义是在预编译的时候把所有的宏名替换,内联函数则是在编译阶段把所有调用内联函数的地方把内联函数插入。这样可以省去函数压栈退栈,提高了效率

44.内联函数与普通函数的区别

1. 内联函数和普通函数的参数传递机制相同,但是编译器会在每处调用内联函数的地方将内联函数内容展开,这样既避免了函数调用的开销又没有宏机制的缺陷。

2. 普通函数在被调用的时候,系统首先要到函数的入口地址去执行函数体,执行完成之后再回到函数调用的地方继续执行,函数始终只有一个复制。

    内联函数不需要寻址,当执行到内联函数的时候,将此函数展开,如果程序中有N次调用了内联函数则会有N次展开函数代码。

3. 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句。如果内联函数函数体过于复杂,编译器将自动把内联函数当成普通函数来执行。

45.C++中成员函数能够同时用static和const进行修饰?

不能。

C++编译器在实现const的成员函数(const加在函数右边)的时候为了确保该函数不能修改类的中参数的值,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。

即:static修饰的函数表示该函数是属于类的,而不是属于某一个对象的,没有this指针。const修饰的函数表示该函数不能改变this中的内容,会有一个隐含的const this指针。两者是矛盾的。

46.溢出,越界,泄漏

1.溢出

要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。

1)栈溢出

a.栈溢出是指函数中的局部变量造成的溢出(注:函数中形参和函数中的局部变量存放在栈上)

栈的大小通常是1M-2M,所以栈溢出包含两种情况,一是分配的的大小超过栈的最大值,二是分配的大小没有超过最大值,但是接收的buff比新buff小(buff:缓冲区, 它本质上就是一段存储数据的内存)

例子1:(分配的的大小超过栈的最大值)

void
{
    char a[99999999999999999];
}

例子2:(接收的buff比新buff小)

void
{
   char a[10] = {0};
   strcpy(a, "abjjijjlljiojohihiihiiiiiiiiiiiiiiiiiiiiiiiiii");
}

注意:调试时栈溢出的异常要在函数调用结束后才会检测到,因为栈是在函数结束时才会开始进行出栈操作

如:

int main(int argc, char* argv[])

{
char a[10] = {0};

strcpy(a, "abjjijjlljiojohihiihiiiiiiiiiiiiiiiiiiiiiiiiii");

exit(0);

return 0;

}

上面情况是检测不到栈溢出的,因为函数还没执行完就退出了

void fun()
{
 char a[10] = {0};
 strcpy(a, "abjjijjlljiojohihiihiiiiiiiiiiiiiiiiiiiiiiiiii");
}
int main(int argc, char* argv[])
{
  fun();
  exit(0);
  return 0;
}

 

这种情况调用完fun函数就会检测到异常了

b.栈溢出的解决办法

如果是超过栈的大小时,那就直接换成用堆;如果是不超过栈大小但是分配值小的,就增大分配的大小

2)内存溢出

使用malloc和new分配的内存,在拷贝时接收buff小于新buff时造成的现象

解决:增加分配的大小

2.越界

越界通常指的是数组越界,如

char a[9]={0};

cout << a[9] << endl;

3.泄漏

这里泄漏通常是指堆内存泄漏,是指使用malloc和new分配的内存没有释放造成的

47.C/C++中分配内存的方法

1) malloc 函数: void *malloc(unsigned int size)

     在内存的动态分配区域中分配一个长度为size的连续空间,如果分配成功,则返回所分配内存空间的首地址,否则返回NULL,申请的内存不会进行初始化。

2)calloc 函数: void *calloc(unsigned int num, unsigned int size)

     按照所给的数据个数和数据类型所占字节数,分配一个 num * size 连续的空间。

    calloc申请内存空间后,会自动初始化内存空间为 0,但是malloc不会进行初始化,其内存空间存储的是一些随机数据。 
3)realloc 函数: void *realloc(void *ptr, unsigned int size)

    动态分配一个长度为size的内存空间,并把内存空间的首地址赋值给ptr,把ptr内存空间调整为size。

    申请的内存空间不会进行初始化。
4)new是动态分配内存的运算符,自动计算需要分配的空间,在分配类类型的内存空间时,同时调用类的构造函数,对内存空间进行初始化,即完成类的初始化工作。动态分配内置类型是否自动初始化取决于变量定义的位置,在函数体外定义的变量都初始化为0,在函数体内定义的内置类型变量都不进行初始化。

 

48.构造函数初始化列表

构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。例如:

class CExample {
public:
    int a;
    float b;
    //构造函数初始化列表
    CExample(): a(0),b(8.8)
    {}
    //构造函数内部赋值
    CExample()
    {
        a=0;
        b=8.8;
    }
};

 

上面的例子中两个构造函数的结果是一样的。上面的构造函数(使用初始化列表的构造函数)显式的初始化类的成员;而没使用初始化列表的构造函数是对类的成员赋值,并没有进行显式的初始化。

初始化和赋值对内置类型的成员没有什么大的区别,像上面的任一个构造函数都可以。对非内置类型成员变量,为了避免两次构造,推荐使用类构造函数初始化列表。但有的时候必须用带有初始化列表的构造函数:
1.成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
2.const成员或引用类型的成员。因为const对象或引用类型只能初始化,不能对他们赋值。 

初始化数据成员与对数据成员赋值的含义是什么?有什么区别?
首先把数据成员按类型分类并分情况说明:
1.内置数据类型,复合类型(指针,引用)
    在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的
2.用户定义类型(类类型)
    结果上相同,但是性能上存在很大的差别。因为类类型的数据成员对象在进入函数体前已经构造完成(先进行了一次隐式的默认构造函数调用),也就是说在成员初始化列表处进行构造对象的工作,调用构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用了拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)。

49.vector中v[i]与v.at(i)的区别

void f(vector<int> &v)
{
  v[5];    // A
  v.at[5];  // B
}

如果v非空,A行和B行没有任何区别。如果v为空,B行会抛出std::out_of_range异常,A行的行为未定义。

c++标准不要求vector<T>::operator[]进行下标越界检查,原因是为了效率,总是强制下标越界检查会增加程序的性能开销。设计vector是用来代替内置数组的,所以效率问题也应该考虑。不过使用operator[]就要自己承担越界风险了。

如果需要下标越界检查,请使用at。但是请注意,这时候的性能也是响应的会受影响,因为越界检查增加了性能的开销。

50.指向函数的指针--函数指针

链接

51.C++中调用C的函数

extern "C"

链接

52.指针常量与常量指针

常量指针(被指向的对象是常量)

定义:又叫常指针,可以理解为常量的指针,指向的是个常量

关键点:

  1. 常量指针指向的对象不能通过这个指针来修改,可是仍然可以通过原来的声明修改;
  2. 常量指针可以被赋值为变量的地址,之所以叫常量指针,是限制了通过这个指针修改变量的值;
  3. 指针还可以指向别处,因为指针本身只是个变量,可以指向任意地址;
const int *p或int const *p

(记忆技巧:const读作常量,*读作指针)

#include <stdio.h>
// 常量指针(被指向的对象是常量)
int main() {
    int i = 10;
    int i2 = 11;
    const int *p = &i;
    printf("%d\n", *p);//10
    i = 9; //OK,仍然可以通过原来的声明修改值,
    //Error,*p是const int的,不可修改,即常量指针不可修改其指向地址
    //*p = 11;  //error: assignment of read-only location ‘*p’
    p = &i2;//OK,指针还可以指向别处,因为指针只是个变量,可以随意指向;
    printf("%d\n", *p);//11
    return 0;
}

 

指针常量(指针本身是常量)

定义:

本质是一个常量,而用指针修饰它。指针常量的值是指针,这个值因为是常量,所以不能被赋值。

关键点

  1. 它是个常量!
  2. 指针所保存的地址可以改变,然而指针所指向的值却不可以改变;
  3. 指针本身是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化;
int* const p;

 

//指针常量(指针本身是常量)
#include <stdio.h>

int main() {
    int i = 10;
    int *const p = &i;
    printf("%d\n", *p);//10
    //Error,因为p是const 指针,因此不能改变p指向的内容
    //p++;//error: increment of read-only variable ‘p’
    (*p)++;    //OK,指针是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化
    printf("%d\n", *p);//11
    i = 9;//OK,仍然可以通过原来的声明修改值,
    return 0;
}

 

 53.防止头文件被重复包含

链接

54.详解拷贝构造函数相关知识

非常好的一篇博客:链接

55.为什么虚函数比普通函数慢?

虚函数在调用时需要通过查找虚函数表的方式来访问,故而比普通函数慢。

56.extern “C”

extern "C"是用来实现C和C++混合编译的。

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。
加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;
而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
假如,某个函数的原型为void foo(int x, int y);该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生_foo_int_int之类的名字。_foo_int_int这样的名字是包含了函数名以及形参,C++就是靠这种机制来实现函数重载的。 

使用场景:

a. C++代码调用C语言代码
b. 在C++的头文件中使用
c. 在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会有用到


a. C++调用C语言代码:

/* c语言头文件:cExample.h */ 
      #ifndef C_EXAMPLE_H 
      #define C_EXAMPLE_H 
      extern int add(int x, int y); 
      #endif
    
      /* c语言的实现文件:cExample.c */ 
      #include "cExample.h" 
      int add(int x, int y) 
      { 
        return x + y; 
      }
    
      /* c++实现文件,调用add:cppFile.cpp */ 
      extern "C" 
      { 
        #include "cExample.h"; 
      } 
      int main() 
      { 
        add(2, 3); 
        return 0; 
      }

b. 在C++头文件中使用。

  // C中引用C++语言中的函数或者变量时,C++的头文件需要加上extern “C”,
  // 但是C语言中不能直接引用声明了extern “C”的该头文件,应该仅在C中将C++中定义的extern “C”函数声明为extern类型。
  /* c++头文件cppExample.h */ 
  #ifndef CPP_EXAMPLE_H 
  #define CPP_EXAMPLE_H 
  extern "C" int add(int x, int y); 
  #endif

  /* c实现文件cFile.c */ 
  extern int add(int x, int y); 
  int main() 
  { 
    add(2, 3); 
    return 0; 
  }


57.结构体与类的区别

a. 实例化的结构体存储在栈内,实例化的类存储在堆内,结构体的效率较高。

b. 结构体没有析构函数。

c. 结构体不可以继承。

d. 结构体的默认成员类型为public,类为private。

  • 6
    点赞
  • 0
    评论
  • 45
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值