C++知识点总结

一、static关键字的作用

  • 静态成员的特点
    (1)static数据成员通常类内声明,类外定义;
    (2)static数据成员只能在类的外部进行初始化;
    (3)static数据成员可以是该成员所属的类类型,而非static数据成员只能自身类的引用或者指针;
    (4) static数据成员可以用作类成员函数的默认实参;
    (5)static数据成员的值可以改变。
  • 静态成员和非静态成员的区别
    (1)静态变量使用static修饰符进行声明,在类被实例化时创建,通过类和对象都可以进行访问;
    (2)不带有static修饰符声明的变量称做非静态变量,在对象被实例化时创建,通过对象访问;
    (3)一个类的所有实例的同一静态变量都是同一个值,同一个类的不同实例的同一非静态变量可以是不同的值;
    (4)静态函数的实现里不能使用非静态成员,如非静态变量、非静态函数等。
  • 静态成员函数的特点
    (1)static成员函数没有this形参,它可以访问所属类的static 成员,但不能访问非static成员;
    (2)static成员函数既可以在类的内部定义,也可以在类的外部定义,在外部定义时,不能重复指定static保留字。

二、C++和C的区别

  • 设计思想上:
    C++是面向对象的语言,而C是面向过程的结构化编程语言
  • 语法上:
    C++具有重载、继承和多态三种特性;
    C++相比C,增加许多类型安全的功能,比如强制类型转换;
    C++支持范式编程,比如模板类、函数模板等

三、 C++中四种cast转换

C++中四种类型转换是:static_cast、dynamic_cast、const_cast、reinterpret_cast。

  • 1、const_cast:对于未定义const版本的成员函数,我们通常需要使用const_cast来去除const引用对象的const,完成函数调用。另外一种使用方式,结合static_cast,可以在非const版本的成员函数内添加const,调用完const版本的成员函数后,再使用const_cast去除const限定;
  • 2、static_cast:完成基础数据类型;同一个继承体系中类型的转换;任意类型与空指针类型void* 之间的转换;
  • 3、dynamic_cast:用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上(指的是子类向基类的转换)和向下转化(指的是基类向子类的转换)。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换;
  • 4、reinterpret_cast:几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;

四、C/C++中指针和引用的区别

  • 引用有自己的一块空间,而引用只是一个别名;
  • 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
  • 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
  • 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
  • 可以有const指针,但是没有const引用;
  • 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
  • 指针可以有多级指针(**p),而引用只有一级;
  • 指针和引用使用++运算符的意义不一样;
  • 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

五、C++中的四个智能指针:shared_ptr、unique_ptr、weak_ptr、auto_ptr

智能指针出现的原因:智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

  • 1、auto_ptr(c++98的方案,c++11已经抛弃)原因是缺乏语言特性如 “针对构造和赋值” 的 std::move 语义,以及其他瑕疵;
  • 2、unique_ptr(替换auto_ptr):是 C++11 才开始提供的类型,是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有着被销毁或变成 empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。实现独占式拥有(exclusive ownership)或严格拥有(strict ownership)概念,保证同一时间内只有一个智能指针可以指向该对象。你可以移交拥有权。它对于避免内存泄漏(resource leak)——如 new 后忘记 delete ——特别有用。unique_ptr 用于取代 auto_ptr;
  • 3、shared_ptr:shared_ptr实现共享式拥有概念。多个智能指针指向相同对象,该对象和其相关资源会在 “最后一个 reference 被销毁” 时被释放。为了在结构较复杂的情景中执行上述工作,标准库提供 weak_ptr、bad_weak_ptr 和 enable_shared_from_this 等辅助类。多个智能指针可以共享同一个对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的所有资源;
  • 4、weak_ptr:weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。因此,在 default 和 copy 构造函数之外,weak_ptr 只提供 “接受一个 shared_ptr” 的构造函数。可打破环状引用(cycles of references,两个其实已经没有被使用的对象彼此互指,使之看似还在 “被使用” 的状态)的问题。

六、野指针

野指针就是指向一个已删除的对象或者未申请访问受限内存区域的指针

七、为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数

  • 将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄露。
  • C++默认的析构函数不是虚函数,因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,只有当需要当做父类是,设置为虚函数。

八、函数指针

  • 定义:函数指针是指向函数的指针变量。
    (1)函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数;
    (2)在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址;
    (3)有了指向函数的指针变量后,可用该指针变量调用函数。
  • 函数指针用途:调用函数和做函数的参数,比如回调函数。
  • 示例:
char *fun(char *p){...}            //指针函数fun
char *(*pf)(char * p);             //函数指针pf
pf=fun;                            //函数指针pf指向函数fun
pf(p);                             //通过函数指针pf调用函数fun

九、fork函数的作用

  • Fork:创建一个和当前进程映像一样的进程可以通过fork()系统调用
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
  • 成功调用fork()会创建一个新的进程,它几乎与调用fork()的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork()调用会返回0。在父进程中fork()返回子进程的pid。如果出现错误,fork()返回一个负值。
  • 最常见的fork()用法是创建一个新的进程,然后使用exec()载入二进制映像,替换当前进程的映象。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。

十、C++中析构函数的作用

  • 析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数;
  • 析构函数不能带任何参数,也没有返回值,只能有一个析构函数,不能重载;
  • 若用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数,它也不进行任何操作,许多简单的类中没有用显示的析构函数;
  • 若类中有指针,且在使用的过程中动态的申请了内存,最好显式构造的析构函数之前,释放掉申请的内存空间,避免内存泄露;
  • 类析构的顺序:1) 派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。

十一、静态函数和虚函数的区别

  • 静态函数在编译的时候就已经确定运行时机;
  • 虚函数在运行的时候动态绑定;
  • 虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。

十二、重载和重写

  • 重载:两个函数名相同,但是参数列表不同(个数、类型),返回值类型没有要求,在同一作用域中;
  • 重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数。

十三、虚函数和多态

  • 多态的实现主要分为静态多态和动态多态;
    静态多态——主要是重载,在编译的时候就已经确定;
    动态多态——用虚函数机制实现的,在运行器件动态绑定;
  • 例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
  • 虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段中。
  • 当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

十四、区别下列四个代码

//字符串“123”保存在常量区,const本来是修饰arr指向的值不能通过arr
//去修改,但是字符串“123”在常量区,本来就不能改变,所以不加const
//效果都一样
const char * arr="123";
//字符串123保存在常量区,这个brr指针指向的是同一个位置,同样不能通过
//brr去修改“123”的值
char *brr="123";
//这里123本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区
const char crr[]="123";
//字符串123保存在栈区,可以通过drr去修改
char drr[]="123";

十五、const修饰成员函数的目的

const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。

十六、C++里是怎么定义常量的?常量存放在内存的哪个位置?

  • 对于局部常量,存放在栈区;
  • 对于全局常量,编译期一般不分配内存,放在符号表中以提高访问效率;字面值常量,比如字符串,放在常量区

十七、new/delete与malloc/free的区别是什么?

  • new/delete 是C++的关键字,malloc/free是C语言的库函数;
  • malloc/free 使用必须指明申请内存空间的大小,对于类类型的对象,后者不会调用构造函数和析构函数。

十八、虚函数表具体是怎样实现运行时多态的?

子类若重写父类虚函数,虚函数表中,该函数的地址会被替换,对于存在虚函数的类的对象,在VS中,对象模型的头部存放指向虚函数表的指针,通过该机制实现多态。

十九、C语言是怎么进行函数调用的

  • 每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。
  • 使用函数能够避免将相同代码重写多次的麻烦,还能减少可执行程序的体积,但也会带来程序运行时间上的开销。
  • 函数调用在执行时
    1° 要在栈中为形参和局部变量分配存储空间;
    2° 还要将实参的值复制给形参;
    3° 还要将函数的返回地址(该地址指明了函数执行结束后,程序应该回到哪里继续执行)放入栈中;
    4° 跳转到函数内部执行。这个过程是要耗费时间的。
  • 函数执行return语句返回是,需要从栈中回收形参和局部变量占用的存储空间,然后从栈中取出返回地址,再跳转到该地址继续执行,这个过程也要耗费时间。
  • 执行到函数调用指令时:
    1° 程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元;
    2° 执行函数代码(也许还需将返回值放入到寄存器中);
    3° 跳回到地址被保存的指令处(这与阅读文章时停下来看脚注,并在阅读完脚注后返回到以前阅读的地方类似)。

二十、C++如何处理返回值?

生成一个临时变量,把它的引用作为函数参数传入函数内。

二十一、C++中拷贝赋值函数的形参能否进行值传递?

不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。如此循环,无法完成拷贝,栈也会满。

二十二、malloc和new区别

  • malloc需要给定申请内存的大小,返回的指针需要强转;
  • new会调用构造函数,不用指定内存大小,返回的指针不用强转。

二十三、fork,wait,exec函数的作用

父进程产生子进程使用fork拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用写实拷贝机制分配内存;exec函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。fork从父进程返回子进程的pid,从子进程返回0;调用了wait的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回-1。exec执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1。

二十四、C++中类成员的访问权限

  • 在类的内部(定义类的代码内部),无论成员被声明为public、protected还是private,都是可以相互访问的,没有访问权限的限制。
  • 在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问public属性的成员,不能访问private、protected属性的成员。

二十五、C++中struct和class的区别

  • 总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
  • 区别:最本质的一个区别就是默认的访问控制
    默认的访问权限:struct是public,class是private的
    struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。

二十六、C++类的内部可以定义引用数据成员吗?

  • 可以,必须通过成员函数初始化列表初始化
class MyClass
{
public:
    MyClass(int &i):a(1),b(i) {} //构造函数初始化列表中是初始化工作
    //在这里做的是赋值而非初始化工作
private:
    const int a;
    int &b;   //引用数据成员b,必须通过列表初始化!
};

二十七、什么是右值引用,跟左值又有什么区别?

  • 左值:能对表达式取地址,或具名对象/对象。一般指表达式结束后依然存在的持久对象。
  • 右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。
  • 右值引用和左值引用的区别
    左值可以寻址,而右值不可以;
    左值可以被赋值,右值不可以被赋值,可以用来给左值赋值;
    左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)。

二十八、C++源文件从文本到可执行文件经历过程

  • 预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件;
  • 编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件;
  • 汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件;
  • 链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件。

二十九、include头文件的顺序以及双引号""和尖括号<>的区别

  • include头文件的顺序:对于include的头文件来说,如果在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.h文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则汇报变量类型未声明错误。
  • 双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。对于使用双引号包含的头文件,查找头文件路径的顺序为:当前头文件目录、编译器设置的头文件路径、系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径;对于使用尖括号包含的头文件,查找头文件的路径顺序为:编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)、系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径。

三十、什么时候会发生段错误?

  • 段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况
    1、使用野指针
    2、试图修改字符串常量的内容

三十一、C++11有哪些新特性?

  • auto关键字:编译器可以根据初始值自动推导出类型,但是不能用于函数传参以及数组类型的推导;
  • nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
  • 智能指针:std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
  • 初始化列表:使用初始化列表来对类进行初始化
  • 右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
  • atomic原子操作用于多线程资源互斥操作
  • 新增STL容器array以及tuple

三十二、const的作用

  • 1、修饰变量,说明该变量不可以被修改
  • 2、修饰指针,分为指向常量的指针(即常量指针)和指针常量
  • 3、常量引用,经常用于形参类型,既避免了拷贝,又避免了函数对值的修改
  • 4、修饰成员函数,说明该成员函数内不能修改成员变量
  • const用法如下:
//类
class A
{
private:
   const int a;     //常对象成员,只能在初始化列表赋值
public:
   //构造函数
   A():a(0){};
   A(int x):a(x){};//初始化列表
   //const可用于对重载函数的区分
   int getValue();        //普通成员函数
   int getValue() const;  //常成员函数,不得修改类中的任何数据成员的值
};
void function()
{
    //对象
    A b;//普通对象,可以调用全部成员函数、更新常成员变量
    const A a;//常对象,只能调用常成员函数
    const A *p=&a;//常指针
    const A &q=a;//常引用
    //指针
    char greeting[]="Hello";   
    char* p1=greeting;      //指针变量,指向字符数组变量
    const char* p2=greeting;  //常量指针即常指针,指针的指向可以改变,但是所存的内容不能变
    char const* p2=greeting;  //与const char* p2等价
    char* const p3=greeting;  //指针常量,指针是一个常量,即指针的指向不能改变,但是指针所存的内容可以改变
    const char* const p4=greeting;  //指向常量的常指针,指针和指针所存的内容都不能改变,本质是一个常量 
}
//函数
void function1(const int Var);  //传递过来的参数在函数内不可变
void function2(const char* Var);  //参数为常量指针即指针所指的内容为常量不能变,指针指向可以改变
void function3(char* const Var); //参数为指针常量
void function4(const int& Var);  //引用参数在函数内为常量

//函数返回值
const int function5();//返回一个常数
const int* function6();//返回一个指向常量的指针变量即常量指针
//使用:const int p=function6();
int const function7();//返回一个指向变量的常指针即指针常量
//使用:int *const p=function7();

三十三、this指针

  • this指针是一个隐含于每一个非静态成员函数 中的特殊指针。它指向调用该成员函数的那个对象。
  • 当对一个对象调用成员函数时,编译程序先将对象的地址赋给this指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用this指针。
  • 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
  • this指针被隐含地声明为:ClassName *const this;这意味着不能给this指针赋值;在ClassName类的const成员函数中,this指针的类型为:
    在以下场景中,经常需要显式引用this指针:
    1、为实现对象的链式引用;
    2、为避免对同一对象进行赋值操作;
    3、在实现一些数据结构时,如list;

三十四、inline内联函数

  • 内联函数是C++为提高程序运行速度所做的一项改进。
  • 内联函数的运行速度比常规函数稍微快,但代价是需要占用更多内存。
  • 内联函数的特点:
    相当于把内联函数里面的内容写在调用内联函数体;
    相当于不用执行进入函数的步骤,直接执行函数体;
    相当于宏,却比宏多了类型检查,真正具有函数特性;
    编译器一般不内联包含循环、递归、switch等复杂操作的内联函数;
    在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数;
  • 内联函数的使用
//声明1(加inline,建议使用)
inline int functionName(int first,int second,...);
//声明2(不加inline)
int functionName(int first,int second,...);
//定义
inline int functionName(int first,int second,...){/****/}
//类内定义,隐式内联
class A
{
    int doA(){return 0;}//隐式内联
};
//类外定义,需要显式内联
class A
{
   int doA();
};
inline int A::doA(){return 0;}//需要显式内联
  • 编译器对内联函数的处理步骤
    • 将 inline 函数体复制到 inline 函数调用点处;
    • 为所用 inline 函数中的局部变量分配内存空间;
    • 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
    • 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)
  • 使用内联函数的优缺点
    • 优点
      1、内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
      2、内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
      3、在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
      4、内联函数在运行时可调试,而宏定义不可以。
  • 缺点
    1、代码膨胀;
    2、inline 函数无法随着函数库升级而升级;
    3、是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。
  • 虚函数可以是内联函数吗?
    • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
    • 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以。
    • inline virtual唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如Base::who()),这只有在编译器具有实际对象而不是对象对象的指针
    • 虚函数内联使用实例如下:
#include <iostream>
using namespace std;

class Base
{
public:
   inline virtual void who()
   {
       cout<<"I am Base"<<endl;
   }
   virtual ~Base(){}
};
class Derived:public Base
{
public:
    inline void who()//不写inline时隐式内联
    {
        cout<<"I am Derived"<<endl;
    }
};
int main()
{
    //此处的虚函数who(),是通过类(Base)的具体对象(b)来调用的
    //编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。
    Base b;
    b.who();
    //此处的虚函数是通过指针调用的,呈现多态性,
    //需要在运行时期间才能确定,所以不能内联。
    Base *ptr=new Derived();
    ptr->who();
    //因为Base有虚析构函数(virtual ~Base(){})
    //所以delete时,会先调用派生类(Derived)析构函数
    //再调用基类(Base)析构函数,防止内存泄露
    delete ptr;
    ptr=nullptr;
    
    system("pause");
    return 0;
}

三十五、volatile关键字

volatile int i=10;
  • volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用volatile告诉编译器不应对这样的对象进行优化。
  • volatile关键字声明的变量,每次访问时都必须从内存中取出值(没有被volatile修饰的变量,可能由于编译器的优化,从CPU寄存器中取值)
  • const可以是volatile(如只读的状态寄存器)
  • 指针可以是volatile

三十六、assert()

  • 断言是宏,而非函数,assert宏的原型定义在<assert.h>(C)、(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义NDEBUG来关闭assert,但是需要在源代码的开头,include<assert.h>之前。
  • **assert()**使用
define NDEBUG  //加上这行,则assert不可用
include <assert.h>
assert(p!=NULL);//assert不可用

三十七、sizeof()运算符

  • sizeof() 对数组,得到整个数组所占空间大小。
  • sizeof() 对指针,得到指针本身所占空间大小。

三十八、#pragma pack(n)

  • 用途:设定结构体、联合以及类成员变量以n字节方式对齐
  • #pragma pack(n)使用实例:
pragma pack(push) //保存对齐状态
pragma pack(4) //设定为4字节对齐
struct test
{
char m1;
double m4;
int m3;
};
pragma pack(pop)//恢复对齐状态

三十九、extern “C

  • 用途:extern "C"的作用是让C++编译器将 extern “C” 声明的代码当作C语言代码处理,可以避免C++因符号修饰导致代码不能和C语言库中的符号进行链接的问题。
  • 被extern限定的函数或变量是extern类型的;被extern "C"修饰的变量和函数是按照C语言方式编译和链接的
  • extern "C"实例如下:
ifdef_cplusplus
extern "C"
{
#endif
void *memset(void*,int,size_t);
ifdef_cplusplus
}
#endif

四十、struct和typedef struct

  • C语言中:
//C语言
typedef struct Student
{
int age;
} S;
//等价于下面
struct Student
{
int age;
};
typedef struct Student S;
  • C++中
    • 1、如果在类标识符空间定义了struct Student{…};,使用Student me;时,编译器将搜索全局标识符表,Student未找到,则在类标识符内搜索。即表现为可以使用Student也可以使用struct Student,如下:
//C++
struct Student
{
   int age;
};

void f(Student me);     //正确,“struct”关键字可省略
  • 2、若定义了与Student同名函数之后,则Student只代表函数,不代表结构体,如下:
typedef struct Student
{
    int age;
} S;

void Student() {}    //正确,定义后“Student”只代表此函数

//void S(){}//错误,符号“S”已经被定义为一个“struct Student”的别名

int main()
{
     Student();
     struct Student me;   //或者"S me";
     return 0;
}

四十一、union联合体

  • 联合(union)是一种节省空间的特殊的类,一个union可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。
  • 联合有如下特点:
    • 默认访问控制符为public
    • 可以含有构造函数、析构函数
    • 不能含有引用类型的成员
    • 不能继承自其他类,不能作为基类
    • 不能含有虚函数
    • 匿名union在定义所在作用域可直接访问union成员
    • 匿名union不能包含protected成员或private成员
    • 全局匿名联合必须是静态(static)的
      -union使用实例如下:
#include <iostream>
using namespace std;

union UnionTest
{
    UnionTest():i(10){};
    int i;
    double d;
};

static union
{
   int i;
   double d;
};

int main()
{
   UnionTest  u;
   union 
   {
      int i;
      double d;
   };
   cout<<u.i<<endl; //输出UnionTest联合的 10

   ::i=20;
   cout<<::i<<endl;  //输出全局静态匿名联合的 20
   i=30;
   cout<<i<<endl;    //输出局部匿名联合的 30

   return 0;
}

四十二、explicit(显式)关键字

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

四十三、friend友元类和友元函数

  • 能访问私有成员、破坏封装性、友元关系不可传递、友元关系的单向性、友元声明的形式及数量不受限制

四十四、::范围解析运算符

  • 种类:
    1、全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
    2、类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
    3、命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的
  • 使用实例:
int  count=0;     //全局(::)的count

class A
{
public:
     static  int count;  //类A的count(A::count)    
};
int main()
{
    ::count=1;     //设置全局的count的值为1
    A::count=2;    //设置类A的count为2
    int count=0;   //局部的count
    count=3;       //设置局部的count的值为3
    
    return 0;
}

四十五、enum枚举类型

  • 限定作用域的枚举类型:
enum class open_modes {input,output,append};
  • 不限定作用域的枚举类型
enum color {red,yellow,green};
enum {floatPrec=6,doublePrec=10};

四十六、decltype关键字

  • 作用和用法:用于检查实体的声明类型或表达式的类型及值分类。
  • 语法:decltype(expression)
  • decltype实例如下:
//尾置返回允许我们在参数列表之后声明返回类型
template <typename It>
auto fcn(It beg,It end)->decltype(*beg)
{
    //处理序列
    return *beg; //返回序列中一个元素的引用
}
//为了使用模板参数成员,必须用typename
template <typename It>
auto fcn2(It beg,It end)->typename remove_reference<decltype(*beg)>::type
{
    //处理序列
    return *beg;  //返回序列中一个元素的拷贝
}

四十七、引用和宏

  • 左值引用:常规引用,一般表示对象的身份

  • 右值引用:右值引用就是必须绑定到右值(一个临时对象,将要销毁的对象)的引用,一般表示对象的值;右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:
    1、 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
    2、能够更简洁明确地定义泛型函数。

  • 引用折叠:X& &、X& &&、X&& &可折叠成X&;X&& &&可折叠成X &&

  • 宏:宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对“参数”进行的是一对一的替换。

四十八、必须使用成员初始化列表的场合

  • 好处:更高效,少了一次调用默认构造函数的过程。
  • 必须要使用初始化列表的场合
    1、常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
    2、引用类型,引用必须在定义的时候初始化,并且补鞥呢重新赋值,所以也要写在初始化列表里面
    3、没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化

四十九、面向对象三大特征

  • 封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
  • public成员:可以被任意实体访问;
  • protected成员:只允许被子类及本类的成员函数访问
  • private成员:只允许被本类的成员函数、友元类或友元函数访问
  • 继承:基类(父类)——>派生类(子类)
  • 多态:即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。多态是以封装和继承为基础的
    • C++多态分类及实现
      1、重载多态(编译期):函数重载、运算符重载
      2、子类型多态(运行期):虚函数
      3、参数多态性(编译期):类模板、函数模板
      4、强制多态(编译期/运行期):基本类型转换、自定义类型转换
    • 静态多态(编译期/早绑定)
//函数重载实例:
class A
{
public:
    void do(int a);
    void do(int a,int b);
};
  • 动态多态(运行期/晚绑定)
    1、虚函数:用virtual修饰成员函数,使其成为虚函数
    2、注意:
    ① 普通函数(非类成员函数)不能是虚函数;
    ②静态函数(static)不能是虚函数
    ③构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)
    ④内联函数不能是表现多态性时的虚函数
    ⑤动态多态实例
class Shape   //形状类
{
public:
    virtual double calcArea(){}
    virtual ~Shape();    
};
class Circle: public Shape     //圆形类
{
publicvirtual  double calcArea();
   ...
};
class Rect:public Shape
{
public:
   virtual double calcArea();
   ...
};

int main()
{
   Shape * shape1=new Circle(4.0);
   Shape * shape2=new Rect(5.0,6.0);
   shape1->calcArea();            //调用圆形类里面的方法
   shape2->calcArea();            //调用矩形类里面的方法
   delete shape1;
   shape1=nullptr;
   delete shape2;
   shape2=nullptr;
   return 0;  
}

五十一、纯虚函数

  • 定义:纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做
  • 用法:virtual int A()=0;

五十二、虚函数、纯虚函数

  • 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样的话,编译器就可以使用后期绑定来达到多态了。
  • 纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
  • 虚函数在子类里面也可以不重载的;但纯虚函数必须在子类去实现。
  • 虚函数的类用于“实作继承”,继承接口的同时也继承了父类的实现。当然大家也可以完成自己的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
  • 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
  • 虚基类是虚继承中的基类。

五十三、虚函数指针、虚函数表

  • 虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。
  • 虚函数表:在程序只读数据段,存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚函数表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。

五十四、虚继承

  • 用途:用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)
  • 底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间);
  • 当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
  • vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;
  • 通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

五十五、虚继承、虚函数

  • 相同点:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
  • 不同点:
  • 虚继承:
    1、虚基类依旧存在继承类中,只占用存储空间
    2、虚基类表存储的是虚基类相对直接继承类的偏移
    • 虚函数
      1、虚函数不占用存储空间
      2、虚函数表存储的是虚函数地址

五十六、类模板、成员函数、虚函数

  • 模板类中可以使用虚函数
  • 一个类(无论是普通类还是类模板)的成员模板(本身是模板的成员函数)不能是虚函数。

五十七、抽象类、接口类、聚合类

  • 抽象类:含有纯虚函数的类
  • 接口类:仅含有纯虚函数的抽象类
  • 聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下特点:
  • 所有成员都是public;
  • 没有定义任何构造函数
  • 没有类内初始化
  • 没有基类,也没有virtual函数

五十八、内存分配和管理

  • malloc、calloc、realloc、alloca
  • malloc:申请指定字节数的内存。申请到的内存中的初始值不确定。
  • calloc:为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为0。
  • realloc:更改以前分配的内存长度(增加或减少)。当增加长度时,可能需要将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定。
  • alloca:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca不具有可移植性,而且在没有传统堆栈的机器上很难实现。alloca不宜使用在必须广泛移植的程序中
  • malloc和free
    • 用途:用于分配、释放内存
    • 使用:
//申请内存,确认是否申请成功
char  *str=(char*) malloc(100);
assert(str!=nullptr);
//释放内存后指针置空
free(p);
p=nullptr;
  • new和delete
  • new/new[]:完成两件事,先底层调用malloc分配了内存,然后调用构造函数(创建对象)。
  • delete/delete[]:也完成了两件事,先调用析构函数(清理资源),然后底层调用free释放空间。
  • new在申请内存时会自动计算所需字节数,而malloc则需要我们自己输入申请内存空间的字节数。
  • 使用:
int main()
{
   T* t=new T();        //先内存分配,再构造函数
   delete t;            //先析构函数,再内存释放
   return 0;     
}

五十九、delete this合法吗?

  • 合法,但是:
  • 必须保证this对象是通过new(不是new[]、不是placement new、不是栈上、不是全局、不是其他对象成员)分配的
  • 必须保证调用delete this 的成员函数时最后一个调用this的成员函数
  • 必须保证成员函数的delete this后面没有调用this了
  • 必须保证delete this后没有人使用了

六十、如何定义一个只能在堆上(栈上)

  • 只能在堆上
  • 方法: 将析构函数设置为私有
  • 原因:C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
  • 只能在栈上
  • 方法:将new和delete重载为私有
  • 原因:在堆上生成对象,使用new关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将new操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。

六十一、强制类型转换运算符(4种)

  • static_cast
  • 特点:静态转换,在编译处理期间。
  • 应用场合: 主要用于C++中内置的基本数据类型之间的转换,但是没有运行时类型的检测来保证转换的安全性。
    • a、用于基类和子类之间的指针或引用之间的转换,这种转换把子类的指针或引用转换为基类表示是安全的;进行下行转换,把积累的指针或引用转换为子类表示时,由于没有进行动态类型检测,所以是不安全的。
    • b、把void类型的指针转换成目标类型的指针(不安全)
    • c、不能用于两个不相关的类型转换
    • d、不能把const对象转换成非const对象
  • const_cast
    • 特点:去常转换,编译时执行。
    • 应用场合:const_cast操作不能在不同的种类间转换。相反,它仅仅把它作用的表达式转换成常量。它可以使一个本来不是const类型的数据转换成const类型的,或者把const属性去掉。
  • reinterpret_cast:
    • 特点:重解释类型转换
    • 应用场合:它有着和C风格强制类型转换同样的功能;它可以转化任何的内置数据类型为其他的类型,同时它也可以把任何类型的指针转化为其他的类型;它的机理是对二进制进行重新的解释,不会改变原来的格式。
  • dynamic_cast < type-id >(expression)
  • 特点:该运算符将expression转换成type_id类型的对象。type_id必须是类的指针,类的引用或者空类型的指针。
  • 应用场合:
    • a、如果type_id是一个指针类型,那么expression也必须是一个指针类型,如果type_id是一个引用类型,那么expression也必须是一个引用类型;
    • b、如果type_id是一个空类型的指针,在运行的时候,就会检测expression的实际类型,结果是一个由expression决定的指针类型;
    • c、如果type_id不是空类型的指针,在运行的时候指向expression对象的指针能否可以转换成type_id类型的指针;
    • d、在运行的时候决定真正的类型,如果向下转换是安全的,就返回一个转换后的指针,若不安全,则返回一个空指针;
    • e、主要用于上下行之间的转换,也可以用于类之间的交叉转换。上行转换时和static_cast效果一样,下行转换时,具有检测功能,比static_cast更安全。

六十二、new delete和malloc free的联系和区别

  • malloc与free是C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
  • 对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
  • C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete,注意new/delete不是库函数。

六十三、hash冲突及解决方法

  • 关键字值不同的元素可能会映射到哈希表的同一地址上就会发生哈希冲突。解决办法:
  • 开放定址法:当冲突发生时,使用某种探查技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存入该地址单元);
  • 再哈希法:同时构造多个不同的哈希函数;
  • 链地址法:将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况;
  • 建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

六十四、多态是什么,多态的作用?

  • 定义:同一个对象,在不同时刻体现出来的不同状态。
  • 多态的前提:
  • 要有继承关系 或实现关系(接口)
  • 要有方法重写
  • 要有父类或者父接口引用指向子类Base b=new Derived();
  • 作用:提高了代码的维护性(继承保证);提高了代码的扩展性

六十五、继承含有纯虚函数的父类,子类能否实例化?

  • 如果父类中存在纯虚函数,子类继承父类时,必须重写父类的纯虚函数,函数名、返回类型、参数个数和类型都不能改。

六十六、构造函数是否可以用private修饰,如果可以,会有什么效果?

  • 如果一个类的构造函数只有一个且为private,这是可以编译通过的;
  • 如果一个类的构造函数只有一个且是private,如果类的内部没有专门创建实例的代码,则是无法创建任何实例的;
  • 如果一个类的构造函数只有一个且是private,如果类的内部有专门创建实例的代码,则只能创建一个或多个实例(根据类内部声明的成员对象个数来定);
  • 如果一个类的构造函数不止一个,private构造函数如果参数为void(无参),则子类无法编译;换言之,如果一个类构造函数只有private且存在子类,则无法编译,除非父类构造函数为public。
  • 如果一个类的构造函数不止一个,private构造函数如果参数为void(无参),则子类无法编译;换言之,如果一个类构造函数只有private且存在子类,则无法编译,除非父类构造函数为public。

六十七、子类的指针能否转换为父类的指针?父类指针能否访问子类成员?

  • 当自己的类指针指向自己类的对象时,无论调用的是虚函数还是实函数,其调用的都是自己的;
  • 当指向父类对象的父类指针被强制转换成子类指针的时候,子类指针调用函数时,只有非重写函数时自己的,虚函数是父类的;
  • 当指向子类对象的子类指针被强制转换成父类指针的时候,也就是父类指针指向子类对象,此时,父类指针调用的虚函数都是子类的,而非虚函数都是自己的;

六十八、虚函数的实现机制

  • C++实现多态的方法
    虚函数在C++中的实现机制就是虚表和虚指针,但是具体是怎样的呢?从more effective C++其中一篇文章里面可以知道:是每个类用了一个虚表,每个类的对象用了一个虚指针。具体用法:
class A
{
public:
     virtual void f();
     virtual void g();
private:
     int a;      
};

class B:public A
{
public:
    void g();
private:
    int b;    
};

A,B的实现省略
因为A有virtual void f(),和g(),所以编译器为A类准备了一个虚表vtableA,内容如下:
在这里插入图片描述
B因为继承了A,所以编译器也为B准备了一个虚表vtableB,内容如下:
在这里插入图片描述
注意:因为B::g是重写了的,所以B的虚表的g放的是B::g的入口地址,但是f是从上面的A继承下来的,所以f的地址是A::f的入口地址。

然后某处有语句B bB;的时候,编译器分配空间时,除了A的int a,B的成员int b;以外,还分配了一个虚指针vptr,指向B的虚表vtableB,bB的布局如下:
在这里插入图片描述
当如下语句的时候:
A *pa = &bB;
pa的结构就是A的布局(就是说用pa只能访问的到bB对象的前两项,访问不到第三项int b)
那么pa->g()中,编译器知道的是,g是一个声明为virtual的成员函数,而且其入口地址放在表格(无论是vtalbeA表还是vtalbeB表)的第2项,那么编译器编译这条语句的时候就如是转换:call *(pa->vptr)[1](C语言的数组索引从0开始哈~)。

这一项放的是B::g()的入口地址,则就实现了多态。(注意bB的vptr指向的是B的虚表vtableB)

另外要注意的是,如上的实现并不是唯一的,C++标准只要求用这种机制实现多态,至于虚指针vptr到底放在一个对象布局的哪里,标准没有要求,每个编译器自己决定。我以上的结果是根据g++ 4.3.4经过反汇编分析出来的。

  • 两种多态实现机制及其优缺点
    除了c++的这种多态的实现机制之外,还有另外一种实现机制,也是查表,不过是按名称查表,是smalltalk等语言的实现机制。这两种方法的优缺点如下:

  • 按照绝对位置查表,这种方法由于编译阶段已经做好了索引和表项(如上面的call *(pa->vptr[1]) ),所以运行速度比较快;缺点是:当A的virtual成员比较多(比如1000个),而B重写的成员比较少(比如2个),这种时候,B的vtableB的剩下的998个表项都是放A中的virtual成员函数的指针,如果这个派生体系比较大的时候,就浪费了很多的空间。

    比如:GUI库,以MFC库为例,MFC有很多类,都是一个继承体系;而且很多时候每个类只是1,2个成员函数需要在派生类重写,如果用C++的虚函数机制,每个类有一个虚表,每个表里面有大量的重复,就会造成空间利用率不高。于是MFC的消息映射机制不用虚函数,而用第二种方法来实现多态,那就是:

  • 按照函数名称查表,这种方案可以避免如上的问题;但是由于要比较名称,有时候要遍历所有的继承结构,时间效率性能不是很高。

六十九、vector和list区别

  • vector拥有一段连续的内存空间,因此支持随机存取。如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector。vector和数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随机存取,但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的拷贝。另外,当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝,这影响vector的效率。
  • list拥有一段不连续的内存空间,因此不支持随机存取。如果需要大量的插入和删除,而不关心随机存取,则应使用list。list是由数据结构中的双向链表实现的,因此它的内存空间可以是不连续的。因此只能通过指针来进行数据的访问,这个特点使得它的随机存取变得非常没有效率,需要遍历中间的元素。但由于链表的特点,它可以很好的效率支持任意地方的删除和插入。

七十、vector的底层实现

  • vector的底层数据结构是一个动态数组。默认构造的大小是0,之后插入按照1 2 4 8 16二倍扩容。
  • 扩容后是一片新的内存,需要把旧内存空间中的所有元素都拷贝进新内存空间中去,之后再在新内存空间中的原数据的后面继续进行插入构造新元素,并且同时释放旧内存空间
  • 由于vector空间的重新配置,导致旧vector的所有迭代器都失效了。vector的初始扩容方式代价太大,初始扩容效率低,需要频繁增长。不仅操作效率比较低,而且频繁的向操作系统申请内存容易造成过多的内存碎片,所以这个时候需要合理使用resize()和reserve()方法提高效率减少内存碎片。

七十一、函数参数压栈方式为什么是从右到左的?

  • 因为C++支持可变函数参数。正是这个原因使得C语言函数参数入栈顺序是从右到左。
  • 具体是:C方式参数入栈顺序的好处就是可以动态变化参数个数。C程序栈底为高地址,栈顶为低地址。函数最左边确定的参数在栈上的位置必须是确定的,否则意味着已经确定的参数是不能定位和找到的,这样是无法保证函数正确执行的。
  • 衡量参数在栈上的位置,就是离开确切的函数调用点有多远。已经确定的参数,它在栈上的位置,不应该依赖参数的具体数量,因此参数数量是未知的!所有只有确定的参数最后入栈才能保证它在栈中的位置是确定的。

七十二、动态库和静态库对比

  • 静态库:静态库对函数库的链接是放在编译时期完成的,程序在运行时与函数库没有关系,便于移植。
    缺点是:浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。文件后缀常为(.a、.lib)
  • 动态库:动态库把对一些库函数的链接载入推迟到程序运行的时期。可以实现进程间的资源共享,将一些程序升级变的简单,直接更改动态库即可。文件后缀常为(.s0、.dll)

七十三、预编译阶段的具体流程

  • 预编译也称为预处理,是做些代码文本的替换工作。
  • 主要处理#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译,就是为编译做预备工作的阶段。
  • 可执行文件编译过程:
    在这里插入图片描述

七十四、黑盒测试与白盒测试

  • 白盒测试:通过程序的源代码进行测试而不使用用户界面。 这种类型的测试需要从代码句法发现内部代码在算法、溢出、路径、条件等等中的缺点或者错误,进而加以修正。
  • 黑盒测试:通过使用整个软件或某种软件功能来严格地测试, 而并没有通过检查程序的源代码或者很清楚地了解该软件的源代码程序具体是怎样设计的。 测试人员通过输入他们的数据然后看输出的结果从而了解软件怎样工作。在测试时,把程序看作一个不能打开的黑盆子,在完全不考虑程序内部结构和内部特性的情况下,测试者在程序接口进行测试,它只检查程序是否能适当地接收和正确的输出。

七十五、一个空类class中有什么?

  • 构造函数、拷贝构造函数、析构函数、赋值运算符重载、取地址操作符重载、被const修饰的取地址操作符重载。

七十六、构造函数和析构函数能被继承吗?

  • 不能
  • 不是所有的函数都能自动地从基类继承到派生类中的。构造函数和析构函数是用来处理对象的创建和析构的,它们只知道对在它们的特殊层次的对象做什么。所以,在整个层次中的所有的构造函数和析构函数都必须被调用,也就是说,构造函数和析构函数不能被继承。
  • 子类的构造函数会显示的调用父类的构造函数或隐式的调用父类的默认的构造函数进行父类部分的初始化。析构函数也一样。它们都是每个类都有的东西,如果能被继承,那就没有办法初始化了。

七十七、构造函数能不能是虚函数?

  • 不能。
  • a.创建一个对象必须明确指出它的类型,否则无法创建,一个对象创建成功编译器获得它的实际类型,然后去调用对应的函数,而如果构造函数声明为虚函数,会形成一个死锁,虚函数是在运行才能确定确定其调用哪一个类型的函数,而具体哪一个类型是编译器通过对象的类型去确定的,但是此时对象还未创建也就没法知道其真实类型。
  • b.虚函数对应一张虚函数表,这个虚函数表是存储在对象的内存空间的,如果构造函数是虚函数就需要通过虚函数表来调用,可是对象还没有实例化,也就是内存空间还没有,找不到虚函数表,所以构造函数是不能声明为虚函数的。

七十八、构造函数和析构函数能不能被重载?

  • 构造函数可以被重载,析构函数不可以被重载。因为构造函数可以有多个且可以带参数, 而析构函数只能有一个,且不能带参数。
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值