C++——基础语法

C++

基础语法

1. 在main执行之前和之后执行的代码

执行之前:

  • 设置栈指针
  • 初始化静态static变量和global全局变量,即.data段的内容
  • 将未初始化部分的全局变量赋初值:数值型shortintlong等为0boolFALSE,指针为NULL等等,即.bss段的内容
  • 全局对象初始化
  • 传参

执行完之后:

  • 全局对象的析构函数会在main函数之后执行;
  • 可以用 atexit 注册一个函数,它会在main 之后执行;
  • __attribute__((destructor))

2.内存对齐

对齐原则

  • 每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍
  • 结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储
  • 结构体的总大小,必须是其内部最大成员的"最宽基本类型成员"的整数倍,不足的要补齐

普通情况,默认的最大对齐方式为最大成员的大小,小于最大成员大小的成员自身大小对齐。

下面案列,a占2字节(补了1字节因为b要从2开始),b占2字节,c占2字节(补了1字节因为结构体总大小),最后再补齐1字节,2 + 2 + 2 = 6

struct Info {
  uint8_t a;
  uint16_t b;
  uint8_t c; 
};

使用alignas可以指定对齐方式

a占2字节(补了1字节因为b要从2开始),b占2字节,c占4字节(补了3字节因为结构体总大小),总共8字节

struct alignas(4) Info2 {
  uint8_t a;
  uint16_t b;
  uint8_t c;
};

如果alignas加到某个成员上,则强制改变该成员的对齐大小,如果改变后的对齐大小是最大的,则结构体总大小也要改变。

a占4字节(补了3字节因为b要从4开始),b占2字节,c占2字节(补了1字节因为结构体大小),总共8字节

struct alignas(4) Info2 {
  uint8_t a;
  alignas(4) uint16_t b;
  uint8_t c;
};

总结:

  • alignas加在结构体上,只改变结构体的对齐大小,即可能需要再最后补0,并不会改成每个成员的对齐大小。

  • alignas加在成员上,会强制改变该成员的对齐大小,如果该成员改变后的对齐大小是最大的,则也会改变结构体的对齐大小。

  • 但如果alignas指定的大小小于默认对齐大小,则会被忽略

需要内存对齐的原因:

为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

如果不进行内存对齐,0号内存放了一个uint8_t, 1-4号内存放了一个int:
假设32位系统,从0号开始访存,如果需要读取int的数据,第一次访存读取到0-3号内存,没有获取到完整的int信息,
因此还需要第二次访存,获取4-8号内存,才能读到完整的int信息,
-------------------------------
|  0  |  1  |  2  |  3  |  4  | 
-------------------------------

3.堆和栈

管理方式堆中资源由程序员控制(可能内存泄漏)栈资源由编译器自动管理
内存管理机制系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删 除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,用于之后释放本内存空间,另外多余的部分会重新放入空闲链表中)只要的剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示栈溢出。
空间大小堆是不连续的内存区域,堆大小受限于计算机系统中有效的虚拟内存(32bit 系统理论上是4G),所以堆的空间比较灵活,比较大栈是一块连续的内存区域,大小是操作系统预定好的
碎片问题对于堆,频繁的new/delete会造成大量碎片,使程序效率降低对于栈,它是有点类似于数据结构上的一个先进后出的栈,进出一一对应,不会产生碎片。(看到这里我突然明白了为什么面试官在问我堆和栈的区别之前先问了我栈和队列的区别)
生长方向堆向上,向高地址方向增长。栈向下,向低地址方向增长。
分配方式堆都是动态分配栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现。
分配效率堆由C/C++函数库提供,机制复杂。所以堆的效率比栈低很多。栈是其系统提供的数据结构,计算机在底层对栈提供支持,分配专门寄存器存放栈地址,栈操作有专门指令。

4.new/delete/malloc/free

new的实现:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的的对象的指针

delete的实现:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存

malloc的实现:仅仅分配内存空间,返回void类型指针

free的实现:仅仅释放内存空间,返回void类型指针(但不是立即返还操作系统,ptmalloc使用双链表保存起来供下一次申请内存时使用)

相同点:
(1)都是申请内存,释放内存

(2)在作用域内,new/malloc所申请的内存,必须被有效释放,否则将会导致内存泄露。

不同点:
(1)malloc与free是C++/C 语言的标准库函数,需要库文件支持,new/delete 是C++的运算符。

(2)对于非内部数据类的对象而言,光用maloc/free 无法满足动态对象的要求。new会调用对应的构造函数,delete会调用对应的析构函数。

5.宏定义define和typedef和函数

宏定义:用于定义常量和书写复杂的内容,预处理阶段完成替换,之后被替换的文本参与编译,相当于直接插入了代码,不进行类型检查。

typedef:用于定义类型别名,会参与编译,会检查数据类型。

函数:在运行时需要跳转到具体调用函数,函数参数具有类型,需要检查类型,并且函数具有返回值。

6.常量指针和指针常量

**指针常量(point to const)**是一个指针,指向一个只读变量,写作int const *p或const int *p

**常量指针(const pointer)**是一个不能改变指向的指针。指针是个常量,必须初始化,一旦初始化完成,它的值就不能在改变了,即不能再指向其他地址。

7.指针和引用

指针是一个变量,存储地址,因此可以指针为空,可以改变指向,可以先声明而不初始化。

引用本质也是一个指针,但它在定义时必须初始化,不能为空,初始化后不能再改变,是原变量的别名。

sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小

在编译器看来, int a = 10; int &b = a; 等价于 int * const b = &a;

传参的注意事项:

  • 需要返回函数内局部变量的内存的时候用指针,而返回局部变量的引用是没有意义的。
  • 栈空间大小比较敏感(比如递归)的时候使用引用,不需要创建临时变量,开销要更小。
  • 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式
  • 指针作为参数进行传递时,形参和实参指向一个地址,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以

8.a和&a有什么区别

先来看

int *p[10]		// 表示一个指针数组, 数组内每个元素都是指向int类型的指针
int (*p)[10]	// 表示一个指针变量, 指向的是一个int类型的数组,数组大小是10
int *p(int)		// 表示一个函数声明,函数名是p,参数是int类型的,返回 int*类型
int (*p)(int)	// 表示一个函数指针,该指针指向的函数具有int类型参数,并且返回值是int类型的

假设有:

int a[10]; 
int (*p)[10] = &a;

其中:

  • a是数组名,是数组首元素地址,+1表示地址值加上一个int类型的大小。如果a的值是0x00000001,加1操作后变为0x00000005
  • &a是数组的指针,其类型为int (*)[10],其加1时,地址值会加上10个int型的大小,值变为数组a尾元素后一个元素的地址

如果输出*((int*)p) ,其值为a[0]的值,因为*被转为int 类型,解引用时按照int类型大小来读取。

9.struct和class的区别

相同点

  • 两者都拥有成员函数、公有和私有部分
  • 任何可以使用class完成的工作,同样可以使用struct完成

不同点

  • 两者中如果不对成员不指定公私有,struct默认是公有的,class则默认是私有的(最重要)
  • struct默认是public继承,而class默认是private继承

C语言的结构体:是用户自定义数据类型(UDT),只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数

C++的结构体:增加了访问权限,且可以和类一样有成员函数。为了和C兼容,成员默认访问说明符为public,被当作类的一种特例。

10.define和const的区别

define:用于定义常量和书写复杂的内容,预处理阶段完成替换,之后被替换的文本参与编译,相当于直接插入了代码,不进行类型检查。

const:在编译、运行的时候起作用,有数据类型,会进行类型安全检查,const定义的变量只是值不能改变,但要分配内存空间。

11.const和static的区别

"static"关键字主要用于控制变量、函数或类成员的作用域和生命周期

"const"关键字用于声明常量,防止其值被修改

const

  • 不考虑类的情况
    • const常量在定义时必须初始化,之后无法更改
    • const形参可以接收const和非const类型的实参,例如// i 可以是 int 型或者 const int 型void fun(const int& i){ //…}
  • 考虑类的情况
    • const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,因为初始化列表是在进入构造函数体之前执行的;不同对象对其const数据成员的值可以不同,所以不能在类中声明时初始化
    • const成员函数:不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值。const对象不可以调用非const成员函数;

static

  • 不考虑类的情况
    • 隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用
    • 默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区
    • 静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用
  • 考虑类的情况
    • static成员变量:只与类关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。
    • static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile(虚函数的调用关系,this->vptr->ctable->virtual function);可以被非static成员函数任意访问

12.顶层const和底层const

  • 顶层const:指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是 * 号的右边
  • 底层const:指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是 * 号的左边
int a = 10; int* const b1 = &a;     // 顶层 const,b1为常量指针,本身是一个常量
const int* b2 = &a;       			// 底层 const,b2为指针常量,本身可变,所指的对象是常量
const int b3 = 20; 		   			// 顶层 const,等价int const b3, b3是常量不可变
const int* const b4 = &a;  			// 前一个const为底层,后一个为顶层,b4不可变,所指的对象也不能变
const int& b5 = a;		   			// 用于声明引用变量,都是底层const

13.数组名和指针(这里为指向数组首元素的指针)

  • 数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。

  • 当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了

14.final和override

final

当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字。

class Base
{
    virtual void foo();
};
 
class A : public Base
{
    void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};

class B final : A // 指明B是不可以被继承的
{
    void foo() override; // Error: 在A中已经被final了
};
 
class C : B // Error: B is final
{
};

override

指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的。

class A
{
    virtual void foo();
}
class B : public A
{
    void foo(); //OK
    virtual void foo(); // OK
    void foo() override; //OK
}

15.拷贝初始化和直接初始化

直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。

拷贝初始化:首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象

string str1("I am a string"); // 语句1 直接初始化
string str2(str1); // 语句2 直接初始化,str1是已经存在的对象,直接调用拷贝构造函数对str2进行初始化
string str3 = "I am a string"; //语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3
string str4 = str1; // 语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数
 

16.初始化和赋值

  • 对于简单类型来说,初始化和赋值没什么区别
  • 对于类和复杂数据类型来说,很大区别
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;
}
 

17.extern"C"

extern "C" 的主要作用是在 C++ 中声明和定义以 C 的方式编写的函数或变量,以避免名称修饰导致的链接错误

哪些情况下使用extern “C”:

(1)C++代码中调用C语言代码;

(2)在C++中的头文件中使用;extern "C" { #include "foo.h" }

(3)在多个人协同开发时,可能有人擅长C语言,而有人擅长C++;

18.野指针和悬空指针

野指针,指的是没有被初始化过的指针

int main(void) { 
    int* p;     // 未初始化
    std::cout<< *p << std::endl; // 未初始化就被使用
    return 0;
}

悬空指针,指针最初指向的内存已经被释放了的一种指针

p和p2就是悬空指针,继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr

int main(void) { 
  int * p = nullptr;
  int* p2 = new int;
  p = p2;
  delete p2; // 释放了p2指向的内存
}

19.C和C++的类型安全

(1)C的类型安全

C只在局部上下文中表现出类型安全,比如试图从一种结构体的指针转换成另一种结构体的指针时,编译器将会报告错误,除非使用显式类型转换。然而,C中相当多的操作是不安全的。

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涉及更多具体的类型检查。

20.重载、重写(覆盖)和隐藏

(1)重载**(overload)**

指在同一个类中的同名成员函数有重载关系,主要特点是函数名相同,参数类型和数目有所不同(水平关系)

注意:可以有不同的返回类型,只要参数列表不同

(2)重写(覆盖)(override)

重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数(垂直关系)

(3)隐藏**(hide)**

隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数,包括以下情况:

  • 两个函数参数相同,但不是虚函数,基类函数都会隐藏
  • 两个函数参数不同,无论基类函数是不是虚函数,基类函数都会被隐藏

21.构造函数的种类

  • 默认构造函数

    Student(){ // 默认构造函数,没有参数
        this->age = 20;
        this->num = 1000;
    };  
    
  • 初始化构造函数(有参数)

    Student(int a, int n):age(a), num(n){}; // 初始化构造函数,有参数和参数列表
    
  • 拷贝构造函数

    • 用类的一个实例化对象去初始化另一个对象的时候会被调用
    • 函数的参数是类的对象时会被调用(非引用传递)
    • 函数的返回值是函数体内局部对象的类的对象时会被调用
    Student(const Student& s){ // 拷贝构造函数,这里与编译器生成的一致
        this->age = s.age;
        this->num = s.num;
    }; 
    
  • 移动构造函数(move和右值引用)

    直接使用原有对象的空间呢,这样就避免了新的空间的分配,需要用std::move将左值转换成右值

     MyObject(MyObject&& other) noexcept {
         std::cout << "Move Constructor" << std::endl;
         // 执行资源移动操作,例如指针的转移等
     }
    
  • 委托构造函数

    它允许一个构造函数调用同一类中的其他构造函数来完成部分或全部的初始化工作。

    Rectangle() : Rectangle(0, 0) {
        std::cout << "Default Constructor" << std::endl;
    }
    
  • 转换构造函数

    Student(int r){  // 转换构造函数,形参是其他类型变量,且只有一个形参,将其他类型的变量隐式转换为本类对象
        this->age = r;
        this->num = 1002;
    };
    

22.浅拷贝和深拷贝

浅拷贝

浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。

深拷贝

深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。

浅拷贝,当对象的name和传入对象的name指向相同的地址

Student(const Student &s){ // 拷贝构造函数
    name = s.name;
};

深拷贝

Student(const Student &s){ // 拷贝构造函数
    name = new char(20);
    memcpy(name, s.name, strlen(s.name));
};

23.内联函数和宏定义

宏定义:用于定义常量和书写复杂的内容,预处理阶段完成替换,之后被替换的文本参与编译,相当于直接插入了代码,不进行类型检查。

内联函数,在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销来提高执行效率,并且进行参数类型检查,具有返回值,可以实现重载。

24.public,protected和private访问和继承

1.访问权限(针对类本身来说)

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

2.继承权限

public继承

公有继承后,派生类无损继承基类public成员和protected成员,但不能访问基类的私有成员。

protected继承

保护继承后,基类的所有public成员和protected成员都成为派生类的protected成员,并且只能被它的派生类成员函数或友元函数访问,基类的私有成员仍然是私有的,无法访问

private继承

私有继承后,基类的所有public成员和protected成员都成为派生类的private成员,并不能被它的派生类的子类所访问,基类的成员只能由自己派生类访问,当前类无法再往下继承

25.判断大小端存储

大端存储:字数据的高字节存储在低地址

小端存储:字数据的低字节存储在低地址

在Socket编程中,往往需要将操作系统所用的小端存储的IP地址转换为大端存储,这样才能进行网络传输

使用强制类型转换,留下低地址的部分

int main()
{
    int a = 0x1234;
    //由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
    char c = (char)(a);
    if (c == 0x12)
        cout << "big endian" << endl;
    else if(c == 0x34)
        cout << "little endian" << endl;
}

26.volatile、mutable和explicit关键字

volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去从它所在的内存重新读取这个变量的值,而不是读寄存器内的备份

如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。

volatile用在如下的几个地方:

  1. 中断服务程序中修改的供其它程序检测的变量需要加volatile;

  2. 多任务环境下各任务间共享的标志应该加volatile;

  3. 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义

mutable修饰的变量,将永远处于可变的状态。跟const是反义词。在const函数里面可以修改被mutable修饰的数据成员,

explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显式的方式进行类型转换

  • explicit 关键字只能用于类内部的构造函数声明上

27.plain new,nothrow new和placement new

(1)plain new

void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void *) throw();

就是我们常用的new,plain new在空间分配失败的情况下,抛出异常std::bad_alloc而不是返回NULL

(2)nothrow new

void * operator new(std::size_t,const std::nothrow_t&) throw();
void operator delete(void*) throw();

nothrow new在空间分配失败的情况下是不抛出异常,而是返回NULL

(3)placement new

void* operator new(size_t, void*);
void operator delete(void*, void*);

这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。

placement new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事情就是调用对象的构造函数

placement new构造起来的对象数组,要显式的调用他们的析构函数来销毁,千万不要使用delete,这是因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。

28.形参与实参

定义:

形参位于函数的参数列表中,只有在函数内部有效,在函数调用结束时, 即刻释放所分配的内存单元。

实参是函数调用中传递给函数的实际值,可以是常量、变量、表达式、函数等。实参应预先用赋值,输入等办法使实参获得确定值

联系:

  • 实参和形参在数量上,类型上,顺序上应严格一致
  • 只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。
  • 当形参和实参不是指针类型时,形参将实参的内容复制一份,在该函数运行结束的时候形参被释放,而实参内容不会改变。

29.值传递、指针传递、引用传递

值传递:有一个形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象或是大的结构体对象,将耗费一定的时间和空间。(传值)

指针传递:同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个固定为4字节的地址。指针传递可以改变形参指针的指向,不影响实参指针,也可以改变实参指针指向的值。

引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名。(传地址)任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量

指针传递和引用传递比值传递效率高。一般主张使用引用传递,代码逻辑上更加紧凑、清晰。

31.什么是类的继承

所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,子类可以拥有父类没有的属性和方法,子类对象可以当做父类对象使用。

30.malloc、realoc、calloc的区别

  1. malloc函数
void* malloc(unsigned int num_size);
int *p = malloc(20 * sizeof(int)); // 申请20个int类型的空间;
  1. calloc函数

省去了人为空间计算;malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;

void* calloc(size_t n,size_t size);
int *p = calloc(20, sizeof(int));
  1. realloc函数

给动态分配的空间分配额外的空间,用于扩充容量

void realloc(void *p, size_t new_size);

31.类成员初始化方式

赋值初始化,通过在函数体内进行赋值初始化,是在所有的数据成员被分配内存空间后才进行的;

列表初始化,使用初始化列表进行初始化,给数据成员分配内存空间时就进行初始化。

初始化列表会快一些的原因是,对于类对象成员,它少了一次调用构造函数的过程,而在函数体中赋值则会多一次默认构造函数的调用。(类对象成员自身的构造)

但对于内置数据类型则没有差别。

32.哪些情况必须用到成员列表初始化

① 当初始化一个引用成员时;

② 当初始化一个常量成员时;

③ 当调用一个基类的构造函数,而它拥有一组参数时;

④ 当调用一个成员类的构造函数,而它拥有一组参数时;

注意:列表中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的;

33.构造函数的执行顺序

上中下

① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。

② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。

③ 类的成员对象的构造函数(按照成员对象在类中的定义顺序)

④ 派生类自己的构造函数

34.string 和 char * 和 const char*

string继承自basic_string,其实是对char进行了封装,封装的string包含了char*数组,容量,长度等等属性。

string可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2*n),然后将原字符串拷贝过去,并加上新增的内容。

a) string转const char*

string s = “abc”; 
const char* c_s = s.c_str(); 

b) const char 转string*

// 直接赋值即可 
const char* c_s = “abc”; 
string s(c_s); 

c)string 转char*

string s = “abc”; 
char* c; 
const int len = s.length(); 
c = new char[len+1]; 
strcpy(c,s.c_str()); 

d) char 转string*

// 和const char* 一样
char* c = “abc”; 
string s(c); 

e) const char 转char**

const char* cpc = “abc”; 
char* pc = new char[strlen(cpc)+1]; 
strcpy(pc, cpc);

f) char 转const char,直接赋值即可**

char* pc = “abc”; 
const char* cpc = pc;

35.异常处理

(1)try、throw和catch关键字

int main()
{
    double m = 1, n = 0;
    try {
        cout << "before dividing." << endl;
        if (n == 0)
            throw - 1;  // 抛出int型异常
        else if (m == 0)
            throw - 1.0;  // 拋出 double 型异常
        else
            cout << m / n << endl;
        cout << "after dividing." << endl;
    }
    catch (double d) {
        cout << "catch (double)" << d << endl;
    }
    catch (...) {
        cout << "catch (...)" << endl;
    }
    cout << "finished" << endl;
    return 0;
}

程序的执行流程是先执行try包裹的语句块,如果执行过程中没有异常发生,则不会进入任何catch包裹的语句块,如果发生异常,则使用throw进行异常抛出,再由catch进行捕获,throw可以抛出各种数据类型的信息,代码中使用的是数字,也可以自定义异常class。

(2)函数的异常声明列表

int fun() throw(int,double,A,B,C){...};

这种写法表示函数可能会抛出int,double型或者A、B、C三种类型的异常。

如果throw中为空,表明不会抛出任何异常;

如果没有throw则可能抛出任何异常。

(3)C++标准异常类 exception

C++ 标准库中有一些类代表异常,这些类都是从 exception 类派生而来的

img

  • bad_typeid:使用typeid运算符,如果其操作数是一个多态类的指针,而该指针的值为 NULL,则会拋出此异常

  • bad_cast:在用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常

  • bad_alloc:在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常

  • out_of_range:用 vector 或 string的at 成员函数根据下标访问元素时,如果下标越界,则会拋出此异常

36.内存泄露

常说的内存泄漏是指堆内存的泄漏

堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。

应用程序般使用malloc、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了

避免内存泄露的几种方式

  • 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
  • 一定要将基类的析构函数声明为虚函数
  • 对象数组的释放一定要用delete []
  • 有new就有delete,有malloc就有free,保证它们一定成对出现

检测工具

  • Linux下可以使用Valgrind工具
  • Windows下可以使用CRT库

37.对象复用的了解,零拷贝的了解

对象复用

通过将对象存储到“对象池”中实现对象的重复利用,这样可以避免多次创建重复对象的开销,节约系统资源。

零拷贝

零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。

如vector的一个成员函数**emplace_back()**很好地体现了零拷贝技术,它跟push_back()函数一样可以将一个元素插入容器尾部

区别在于:使用push_back()函数需要调用拷贝构造函数和转移构造函数,而使用emplace_back()插入的元素原地构造,不需要触发拷贝构造和转移构造,效率更高。

struct Person
{
    string name;
    int age;
    // 初始构造函数
    Person(string p_name, int p_age): name(std::move(p_name)), age(p_age)
    {
         cout << "I have been constructed" <<endl;
    }
     // 拷贝构造函数
     Person(const Person& other): name(std::move(other.name)), age(other.age)
    {
         cout << "I have been copy constructed" <<endl;
    }
     // 转移构造函数
     Person(Person&& other): name(std::move(other.name)), age(other.age)
    {
         cout << "I have been moved"<<endl;
    }
};

int main()
{
    vector<Person> e;
    cout << "emplace_back:" <<endl;
    e.emplace_back("Jane", 23); //不用构造类对象

    vector<Person> p;
    cout << "push_back:"<<endl;
    p.push_back(Person("Mike",36));
    return 0;
}
//输出结果:
//emplace_back:
//I have been constructed
//push_back:
//I have been constructed
//I am being moved.

emplace_back()在插入元素时可以避免额外的复制操作,从而提供更好的性能,并且更方便地使用构造函数的参数列表进行构造。push_back()则需要先创建对象,然后再将其复制或移动到容器中。

38.介绍面向对象的三大特性

三大特性:继承、封装和多态

(1)封装

数据和代码捆绑在一起,避免外界干扰和不确定性访问。

封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。

(2)继承

让某种类型对象获得另一个类型对象的属性和方法。

它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

常见的继承有三种方式:

  1. 实现继承:指使用基类的属性和方法而无需额外编码的能力
  2. 接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力
  3. 可视继承:指子窗体(类)使用基窗体(类)的外观和实现代码的能力(C++里好像不怎么用)

(3)多态

同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为

(重载实现编译时多态,虚函数实现运行时多态)

多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单一句话:允许将子类类型的指针赋值给父类类型的指针

实现多态有二种方式:覆盖(override),重载(overload)。

覆盖:是指子类重新定义父类的虚函数的做法。

重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。

39.四种强制转换reinterpret_cast/const_cast/static_cast /dynamic_cast

reinterpret_cast

reinterpret_cast<type-id> (expression)

type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以用于类型之间进行强制转换。

即进行底层的二进制位重新解释。它对于类型之间的转换非常灵活,可以将任何指针类型转换为任何其他指针类型,甚至可能会导致未定义行为

int* pInt = reinterpret_cast<int*>(0x12345678);

const_cast

const_cast<type_id> (expression)

该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。用法如下:

  • 常量指针被转化成非常量的指针,并且仍然指向原来的对象
  • 常量引用被转换成非常量的引用,并且仍然指向原来的对象
  • const_cast一般用于修改底指针。如const char *p形式
const int x = 5;
int* p = const_cast<int*>(&x);
*p = 10; // Undefined behavior! Trying to modify a const object.

static_cast

static_cast < type-id > (expression)

该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:

  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用引用的转换

    • 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
    • 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的
  • 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。

  • 把空指针转换成目标类型的空指针

  • 把任何类型的表达式转换成void类型

dynamic_cast

dynamic_cast <type-id> (expression)

该运算符把expression转换成type-id类型的对象。type-id 必须是类的指针、类的引用或者void*

有类型检查,基类向派生类转换比较安全,但是派生类向基类转换则不太安全。

dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换

在进行上行转换时,dynamic_cast和static_cast的效果是一样的

在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全

40.C++函数调用的压栈过程

int f(int n) 
{
	cout << n << endl;
	return n;
}

void func(int param1, int param2)
{
	int var1 = param1;
	int var2 = param2;
	printf("var1=%d,var2=%d", f(var1), f(var2));//如果将printf换为cout进行输出,输出结果则刚好相反
}

int main(int argc, char* argv[])
{
	func(1, 2);
	return 0;
}

当函数从入口函数main函数开始执行时,编译器会将我们操作系统的运行状态,main函数的返回地址、main的参数、mian函数中的变量、进行依次压栈;

当main函数开始调用func()函数时,编译器此时会将main函数的运行状态进行压栈,再将func()函数的返回地址、func()函数的参数从右到左、func()定义变量依次压栈;

当func()调用**f()**的时候,编译器此时会将func()函数的运行状态进行压栈,再将f()的返回地址、f()函数的参数从右到左、f()定义变量依次压栈

函数f(var2)、f(var1)依次入栈,而后先执行f(var2),再执行f(var1),最后打印整个字符串,将栈中的变量依次弹出,最后主函数返回。

注意:c/c++中没有规定函数参数的计算顺序,这个和编译器有关,代码参数的计算顺序决定了实际输出。

文字描述:

1、调用者函数从右向左依次把被调函数所需要的参数压入栈;

2、调用者函数使用call指令调用被调函数,并把call指令的下一条指令的地址当成返回地址压入栈中(这个压栈操作

隐含在call指令中);

3、在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即:当前被调函数的栈底地址(mov ebp,esp);

4、在被调函数中,从ebp的位置处开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的

顺序依次减小,即:这些变量的地址是按照栈的延伸方向排列的,先定义的变量先入栈,后定义的变量后入栈;

41.coredump问题

coredump是程序由于异常或者bug在运行时异常退出或者终止,在一定的条件下生成的一个叫做core的文件,这个core文件会记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息等等。对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息。

编译

g++ coredumpTest.cpp -g -o coredumpTest

运行

./coredumpTest

使用gdb调试coredump

gdb [可执行文件名] [core文件名]

42.临时变量作为返回值时的处理过程

C语言里规定:

16bit程序中,返回值保存在ax寄存器中,

32bit程序中,返回值保持在eax寄存器中,

64bit程序中返回值,edx寄存器保存高32bit,eax寄存器保存低32bit

函数调用结束后,临时变量可能被销毁,但返回值被临时存储到寄存器中,并没有放到堆或栈中,因此如果我们需要返回值,一般使用赋值语句就可以了。

43.如何获得结构成员相对于结构开头的字节偏移量

使用<stddef.h>头文件中的,offsetof宏。

#include <iostream>
#include <stddef.h>
using namespace std;

struct  S
{
	int x;   // 0-3
	char y;  // 4-7
	int z;   // 8-15
	double a; // 16
};
int main()
{
	cout << offsetof(S, x) << endl; // 0
	cout << offsetof(S, y) << endl; // 4
	cout << offsetof(S, z) << endl; // 8
	cout << offsetof(S, a) << endl; // 16,前面补了4字节
	return 0;
}

44.静态类型和动态类型,静态绑定和动态绑定

  • 静态类型:对象在声明时采用的类型,在编译期既已确定;

  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;

  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;

  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

    非虚函数一般都是静态绑定,而虚函数都是动态绑定

    class A
    {
    public:
    	/*virtual*/ void func() { std::cout << "A::func()\n"; }
    };
    
    class B : public A
    {
    public:
    	void func() { std::cout << "B::func()\n"; }
    };
    
    class C : public A
    {
    public:
    	void func() { std::cout << "C::func()\n"; }
    };
    
    int main()
    {
    	C* pc = new C(); // pc的静态类型是它声明的类型 C*,动态类型也是 C*;
    	B* pb = new B(); // pb的静态类型和动态类型也都是 B*;
    	A* pa = pc;      // pa的静态类型是它声明的类型A*,动态类型是pa所指向的对象pc的类型C*;
    	pa = pb;         // pa的动态类型可以更改,现在它的动态类型是B*,但其静态类型仍是声明时候的A*;
    	C *pnull = NULL; // pnull的静态类型是它声明的类型C*,没有动态类型,因为它指向了NULL;
        
       	// 绑定类型
        pa->func();      // A::func() pa的静态类型永远都是A*,不管其指向的是哪个子类,都是直接调用A::func()
    	pc->func();      // C::func() pc的动、静态类型都是C*,因此调用C::func();
    	pnull->func();   // C::func() 不用奇怪为什么空指针也可以调用函数,因为这在编译期就确定了,和指针空不空没关系;
    	return 0;
    }
    // 如果 func() 是虚函数,就根据动态类型来调用
    pa->func();    // B::func() 因为有了virtual虚函数特性,pa的动态类型指向B*,因此先在B中查找,找到后直接调用;
    pc->func();    // C::func() pc的动、静态类型都是C*,因此也是先在C中查找;
    pnull->func(); // 空指针异常,因为是func是virtual函数,因此对func的调用只能等到运行期才能确定,然后才发现pnull是空指针;
    
    

    建议:

    绝对不要重新定义继承而来的非虚函数(《Effective C++ 第三版》条款36),因为这样导致函数调用由对象声明时的静态类型确定了,而和对象本身脱离了关系,没有多态,也这将给程序留下不可预知的隐患和莫名其妙的BUG;

45.引用是否能实现动态绑定

可以。引用在创建的时候必须初始化,在访问虚函数时,编译器会根据其所绑定的对象类型决定要调用哪个函数。注意只能调用虚函数


class Base 
{
public:
	virtual void  fun()
	{
		cout << "base :: fun()" << endl;
	}
};

class Son : public Base
{
public:
	virtual void  fun()
	{
		cout << "son :: fun()" << endl;
	}
	void func()
	{
		cout << "son :: not virtual function" <<endl;
	}
};

int main()
{
	Son s;
	Base& b = s; // 基类类型引用绑定已经存在的Son对象,引用必须初始化
	s.fun(); //son::fun()
	b.fun(); //son::fun()
	return 0;
}

需要说明的是虚函数才具有动态绑定,上面代码中,Son类中还有一个非虚函数func(),这在b对象中是无法调用的,如果使用基类指针来指向子类也是一样的。

46.全局变量和局部变量

生命周期不同:全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;

使用方式不同:通过声明后全局变量分配在全局数据段,在程序的各个部分都可以用到;局部变量分配在堆栈区,只能在局部使用。

47.指针加减计算

指针加减本质是对其所指地址的移动,移动的步长跟指针的类型是有关系的。

int main()
{
	int *a, *b, c;
	a = (int*)0x500;
	b = (int*)0x520;
	c = b - a; // 0x20,相当于十进制的32,那么b和a相差8个int的地址单位
	printf("%d\n", c); // 8
	a += 0x020; // a + 32个int地址单位,实际a指向了0x580
	c = b - a;
	printf("%d\n", c); // -24 0x520 - 0x580 = -0x60相当于十进制-96,也就是相差24个地址单位
	return 0;
}

48.判断两个浮点数是否相等

不能直接用==来判断,会出错!

对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!浮点数与0的比较也应该注意。

bool float_equals(float a, float b, float epsilon = 1e-6) {
    return std::abs(a - b) < epsilon;
}

49.继承机制中对象之间如何转换?指针和引用之间如何转换?

将派生类指针或引用转换为基类的指针或引用被称为向上类型转换,向上类型转换会自动进行,而且向上类型转换是安全的。

将基类指针或引用转换为派生类指针或引用被称为向下类型转换,向下类型转换不会自动进行,因为一个基类对应几个派生类,所以向下类型转换时不知道对应哪个派生类,所以在向下类型转换时必须加动态类型识别技术。RTTI技术,用dynamic_cast进行向下类型转换。

50.组合和继承

组合

组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。

组合的优点:

​ ① 当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象时不可见的。

​ ② 当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。

​ ③ 当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。

组合的缺点:

​ ① 容易产生过多的对象。

​ ② 为了能组合多个对象,必须仔细对接口进行定义。

继承

继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person

继承的优点

  • 子类可以重写父类的方法来方便地实现对父类的扩展

继承的缺点有以下几点:

  • 父类的内部细节对子类是可见的。

  • 子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。

  • 如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。

51.函数指针

函数与数据项相似,函数也有地址。一个函数地址是该函数的进入点,也就是调用函数的地址。函数指针即指向这个地址。

int (*pf)(const int&, const int&),pf就是一个函数指针,可以指向返回类型为int,并带有两个const int&参数的函数。

int *pf(const int&, const int&)是一个返回类型为int *, 带有两个const int&参数的函数。

52.结构体变量比较是否相等

  1. 正常需要重载 “==” 操作符来比较结构体变量是否相等

    struct foo {
      int a;
      int b;
      bool operator==(const foo& rhs) *//* *操作运算符重载*
      {
        return( a == rhs.a) && (b == rhs.b);
      }
    };
    
  2. 元素的话,一个个比

  3. 如果指针直接比较,保存的是同一个实例地址,则(p1==p2)为真,否则为假

53.printf函数的实现原理

printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了,

下面给出printf(“%d,%d”,a,b);(其中a、b都是int型的)的汇编代码.

section .data
    format db "%d,%d", 0

section .text
    global main
    extern printf

main:
    ; 分配空间给变量 a 和 b
    push ebp
    mov ebp, esp
    sub esp, 8

    ; 将 a 和 b 的值存储到栈上
    mov dword [ebp-4], 10   ; 假设 a = 10
    mov dword [ebp-8], 20   ; 假设 b = 20

    ; 调用 printf 函数
    push dword [ebp-8]      ; 将 b 的值压入栈
    push dword [ebp-4] 		; 将 a 的值压入栈
    push format             ; 将格式字符串地址压入栈
    call printf             ; 调用 printf 函数
    add esp, 12             ; 清理栈上的参数

    ; 恢复栈并退出程序
    mov esp, ebp
    pop ebp
    ret

54.为什么模板类一般都是放在一个h文件

模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。

所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。

模板类的定义通常需要在每个使用它的源文件中进行实例化。如果将模板类的定义放在源文件中,会导致多个源文件中出现相同的定义,从而引发链接错误。将模板类的定义放在头文件中可以避免这个问题。

55.cout和printf

cout<<是类std::ostream的全局对象。cout<<已存在针对各种类型数据的重载,所以会自动识别数据的类型,输出过程会首先将输出字符放入缓冲区,然后输出到屏幕。

cout << "abc " < <endl; 
cout << "abc\n "; cout  << flush; //这两个才是一样的.flush立即强迫缓冲输出。

printf是行缓冲输出,不是无缓冲输出。

56.重载运算符

  1. 运算符重载函数的命名: 在C++中,运算符重载函数的命名遵循特定的模式。对于大多数运算符,重载函数的名称为"operator"后跟运算符的符号。例如,"+“运算符的重载函数名称为"operator+”
  2. 运算符重载函数的参数: 运算符重载函数通常作为类的成员函数或全局函数进行定义。对于成员函数,第一个参数是隐式的,表示调用该运算符的对象。对于全局函数,两个操作数都作为显式参数传递。
  3. 运算符重载函数的返回类型: 运算符重载函数的返回类型通常是根据运算符的语义和预期行为来确定的。例如,"+"运算符的重载函数可能返回两个操作数相加的结果。

一元运算符重载

// 前置递增操作符
MyClass& operator++() {
    // 实现前置递增操作
    return *this;
}

// 后置递增运算符重载
MyClass operator++(int) {
    MyClass temp = *this;
    // 实现递增操作
    return temp;
}

二元运算符重载

MyClass operator+(const MyClass& other) {
    MyClass result;
    // 实现加法操作
    return result;
}

57.定义和声明

如果是指变量的声明和定义: 从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义就是分配了内存

如果是指函数的声明和定义: 声明:一般在头文件里,对编译器说:这里我有一个函数叫function() 让编译器知道这个函数的存在。 定义:一般在源文件里,具体就是函数的实现过程写明函数体。

58.全局变量和static变量

全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。

但两者的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个原文件组成时,非静态的全局变量在各个源文件中都是有效的。

静态全局变量只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。

59.隐式转换

所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为。

基本数据类型的隐式转换以取值范围的作为转换基础(保证精度不丢失)。隐式转换发生在从小->大的转换中。比如从char转换为int。从int->long。

子类对象可以隐式的转换为父类对象。

如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制。可以通过将构造函数声明为explicit加以制止隐式类型转换,关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit。

60.交换两个数

1)  算术

x = x + y;
y = x - y;
x = x - y; 

2)  异或
// 对于任意一个数x,有 x^x = 0,即同一个数异或自身结果为0。
// 对于任意一个数x,有0^x = x,即0与任意一个数异或结果为该数本身。
x = x^y;// 只能对int,char..
y = x^y;
x = x^y;
// 或者
x ^= y ^= x;
y ^= x;

61.strcpy、sprintf与memcpy

实现功能不同

① strcpy主要实现字符串变量间的拷贝

② sprintf主要实现其他数据类型格式到字符串的转化

③ memcpy主要是内存块间的拷贝。

操作对象不同

① strcpy的两个操作对象均为字符串,遇到被复制字符的**串结束符"\0"**结束

② sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串

③ memcpy的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型,根据其第3个参数决定复制的长度。

执行效率不同

memcpy最高,strcpy次之,sprintf的效率最低。

62.int main(int argc, char *argv[])时的内存结构

  • argc (argument count):表示命令行参数的数量。它是一个整数,包括程序名称在内的所有参数个数。
  • argv (argument vector):是一个指向指针的指针,用于存储命令行参数的字符串。每个指针指向一个参数字符串,其中第一个指针指向程序名称,后续的指针指向其他参数(如果有的话)。

例如,如果你在终端运行一个程序 ./program arg1 arg2,那么 argc 的值将为 3,表示有三个参数(程序名称、arg1 和 arg2),而 argv 的指针数组将包含以下内容:

  • argv[0] 指向程序名称 "./program"
  • argv[1] 指向参数字符串 "arg1"
  • argv[2] 指向参数字符串 "arg2"

可以通过循环遍历 argv 数组来获取和处理命令行参数。通常,argv[0] 是程序的名称,而其他的 argv[i] (其中 0 < i < argc)是用户提供的命令行参数。

63.空类会默认添加哪些函数?

1)  Empty(); // 默认构造函数//
2)  Empty( const Empty& ); // 拷贝构造函数//
3)  ~Empty(); // 析构函数//
4)  Empty& operator=( const Empty& ); // 赋值运算符//

64.类如何实现只能静态分配和只能动态分配

类的实例可以通过**静态分配(栈上分配)动态分配(堆上分配)**来创建

静态分配可以把new、delete运算符重载为private属性,只有使用new运算符,对象才会被建立在堆上,限制new运算符就可以实现类对象只能建立在栈上。

// 1.可以在函数内部或作用域内部声明一个 A 类的对象,它将自动分配在栈上
void someFunction() 
{
    // 在栈上分配 A 对象
    A a;  
    // 使用 a 对象...
}  	// a 对象在作用域结束时自动释放

// 2. 使用静态存储区:可以在全局范围或命名空间范围内声明一个静态变量,该变量将在程序启动时分配,并在程序结束时释放。例如:
static A globalA;  // 在静态存储区分配 A 对象

动态分配是把构造、析构函数设为protected属性,再用子类来动态创建

65.C++中标准库

img

(1)C库:由C标准库扩展而来,强调结构、函数和过程,不支持面向对象技术。

(2)标准的C++库:包括C++ I/O库、String库、数值库等

(3)标准模板库(STL):高效的C++程序库。该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。

66.什么情况用指针当参数,什么时候用引用

对于使用引用的值而不做修改的函数:

如果数据对象很小,如内置数据类型或者小型结构,则按照值传递;

如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针;

如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间;

如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递);

对于修改函数中数据的函数:

如果数据是内置数据类型,则使用指针

如果数据对象是结构,则使用引用或者指针

如果数据是类对象,则使用引用

67.引用的好处和限制

引用传参的好处:

1)在函数内部可以对此参数进行修改

2)提高函数调用和运行的效率

3)提高代码可读性。使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;

但是有以下的限制:

1)不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁

2)不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况,又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak

3)可以返回类成员的引用,但是最好是const。因为如果其他对象可以获得该属性的非常量的引用,那么对该属性的单纯赋值就会破坏业务规则的完整性。

68.阻止一个类被实例化

  1. 将类定义为抽象基类或者将构造函数声明为private
  2. 不允许类外部创建类对象,只能在类内部创建对象

69.如何禁止程序自动生成拷贝构造函数

  1. 为了避免调用拷贝构造函数和拷贝赋值函数,我们需要将他们设置成private,防止被调用。

定义一个base类,在base类中将拷贝构造函数和拷贝赋值函数设置成private,那么派生类中编译器将不会自动生成这两个函数,且由于base类中该函数是私有的,因此,派生类将阻止编译器执行相关的操作。

70.Debug和Release的区别

  • Debug调试版本,包含调试信息,所以容量比Release大很多,并且不进行任何优化。Debug模式下生成两个文件,除了.exe或.dll文件外,还有一个.pdb文件,该文件记录了代码中断点等调试信息;
  • Release不对源代码进行调试,编译时对应用程序的速度进行优化,使得程序在代码大小和运行速度上都是最优的。Release模式下生成一个文件.exe或.dll文件。

71.写一个比较大小的模板函数

template<typename type1,typename type2>//函数模板 
type1 Max(type1 a,type2 b) 
{ 
   return a > b ? a : b; 
} 

void main() 
 { 
  cout<<"Max = "<<Max(5.5,'a')<<endl; 
} 

72.strcpy函数和strncpy函数

char* strcpy(char* strDest, const char* strSrc)
char *strncpy(char *dest, const char *src, size_t n)
  • strcpy函数: 如果参数 dest 所指的内存空间不够大,可能会造成缓冲溢出(buffer Overflow)的错误情况,在编写程序时请特别留意,或者用strncpy()来取代。
  • strncpy函数:用来复制源字符串的前n个字符,src 和 dest 所指的内存区域不能重叠,且 dest 必须有足够的空间放置n个字符。

如果目标长>指定长>源长,则将源长全部拷贝到目标长,自动加上’\0’

如果指定长<源长,则将源长中按指定长度拷贝到目标字符串,不包括’\0’

73.static_cast比C语言中的转换强在哪里

  1. 类型检查:static_cast在进行类型转换时会进行编译时类型检查,以确保转换的类型是合法的。如果转换的类型不合法,则会在编译时发出错误。

  2. 安全性:static_cast会进行隐式转换时的安全检查,确保转换不会导致数据丢失或溢出。它在转换时会执行一些限制,例如将指针类型转换为另一种类型时,会检查是否存在相关的基类和派生类关系。

  3. 支持更多类型转换:static_cast可以执行基本类型之间的转换,还可以将派生类指针与基类指针进行相互转换

74.memset(this,0,sizeof(*this))会发生什么

memset(this, 0, sizeof *this),会将整个对象的内存全部置为0,不用手动一句句地手动初始化。

但要注意:

如果类含有虚函数表,这么做会破坏虚函数表,后续对虚函数的调用都将出现异常;

如果类中含有C++类型的对象:例如,类中定义了一个list的对象,由于在构造函数体的代码执行之前就对list对象完成了初始化,假设list在它的构造函数里分配了内存,那么我们这么一做就破坏了list对象的内存。

75.回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。

为此,你需要做三件事:1,声明;2,定义;3,设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用;

#include <iostream>

// 定义一个回调函数类型
typedef void (*CallbackFunction)(int);

// 定义一个函数,接收一个回调函数作为参数
void PerformOperation(int data, CallbackFunction callback) {
  // 进行一些操作
  if (data % 2 == 0) {
    // 调用回调函数并传递操作结果
    callback(data * 2);
  } else {
    callback(data);
  }
}

// 回调函数的实现
void Callback(int result) {
  std::cout << "回调函数被调用,操作的结果是:" << result << std::endl;
}

int main() {
  int data = 5;
  // 调用函数,并传递回调函数作为参数
  PerformOperation(data, Callback);
  return 0;
}

76.一致性哈希

一致性哈希是一种哈希算法,就是在移除或者增加一个结点时,能够尽可能小的改变已存在key的映射关系

img

一致性hash的基本思想就是使用相同的hash算法将数据和结点都映射到图中的环形哈希空间中

object1-object4需要存在服务器上,沿着顺时针方向寻找,找到的第一个结点(服务器)则将数据存在这个结点上

移除结点

如果一台服务器出现问题,如上图中的nodeB,则受影响的是其逆时针方向至下一个结点之间的数据,只需将这些数据映射到它顺时针方向的第一个结点上即可,下左图

img

添加结点

如果新增一台服务器nodeD,受影响的是其逆时针方向至下一个结点之间的数据,将这些数据映射到nodeD上即可,见上右图

虚拟结点

假设仅有2台服务器:nodeA和nodeC,nodeA映射了1条数据,nodeC映射了3条,这样数据分布是不平衡的。

引入虚拟结点,假设结点复制个数为2,则nodeA变成:nodeA1和nodeA2,nodeC变成:nodeC1和nodeC2,映射情况变成如下:

img

77.C++从代码到可执行程序经历了什么

(1)预编译 .i

主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下:

  1. 删除所有的#define,展开所有的宏定义
  2. 处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
  3. 处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他 文件。
  4. 删除所有的注释,“//”和“/**/”。
  5. 保留所有的**#pragma** 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重 复引用。
  6. 添加行号和文件标识

(2)编译

把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应 的汇编代码文件。

  1. 词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
  2. 语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。
  3. 语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进 行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定 的语义。
  4. 优化:源代码级别的一个优化过程。
  5. 目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言 表示。
  6. 目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移 来替代乘法运算、删除多余的指令等。

(3)汇编

将汇编代码转变成机器可以执行的指令(机器码文件)。 汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过 来,汇编过程有汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Linux 下)、xxx.obj(Window下)

(4)链接

将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接:

  • 静态链接

函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。

空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个 目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;

更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。

但是静态链接的优点就是运行速度快,在可执行程序中已经具备了所有执行程序所需要的任何东西。

  • 动态链接

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多份副本,而是这多个程序在执行时共享同一份副本;

更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。

性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

78.友元函数和友元类的基本情况

通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。但同时也破坏了类的封装性和数据的隐藏性

1)友元函数

有元函数是定义在类外的普通函数,不属于任何类,可以访问其他类的私有成员。但是需要在类的定义中声明所有可以访问它的友元函数。一个函数可以是多个类的友元函数,但是每个类中都要声明这个函数。

class A
{
public:
    friend void set_show(int x, A &a);      //该函数是友元函数的声明
private:
    int data;
};

void set_show(int x, A &a)  //友元函数定义,为了访问类A中的成员
{
    a.data = x;
    cout << a.data << endl;
}

2)友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。但是另一个类里面也要相应的进行声明

class A
{
public:
   friend class C;   //这是友元类的声明,声明C是我兄弟
private:
   int data;
};

class C             //友元类定义,为了访问类A中的成员
{
public:
   void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;}
};

(1) 友元关系不能被继承

(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。

(3) 友元关系不具有传递性。

79.为什么友元函数必须在类内部声明

友元函数不一定要在类内声明,普通的友元函数可以在类外声明,也可以在类内声明。

只有友元工厂才必须用到类内声明友元函数。

80.用C语言实现C++的继承

typedef void (*FUN)();   //定义一个函数指针来实现对成员函数的继承
struct _A    //父类
{
  FUN _fun;  //由于C语言中结构体不能包含函数,故只能用函数指针在外面实现
  int _a;
};

struct _B     //子类
{
  _A _a_;   //在子类中定义一个基类的对象即可实现对父类的继承
  int _b;
};

void _fA()    //父类的同名函数
{
  printf("_A:_fun()\n");
}·

void _fB()    //子类的同名函数
{
  printf("_B:_fun()\n");
}

void test()
{
   _A _a;  //定义一个父类对象_a

  _B _b;  //定义一个子类对象_b

  _a._fun = _fA;    //父类的对象调用父类的同名函数

  _b._a_._fun = _fB;  //子类的对象调用子类的同名函数



  _A* p2 = &_a;  //定义一个父类指针指向父类的对象

  p2->_fun();   //调用父类的同名函数

  p2 = (_A*)&_b; //让父类指针指向子类的对象,由于类型不匹配所以要进行强转

  p2->_fun();   //调用子类的同名函数
}

81.动态编译与静态编译

静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库;

动态编译的可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令。

所以其优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。

缺点是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。

82.几种典型的锁

读写锁

  • 多个读者可以同时进行读
  • 写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
  • 写者优先于读者

互斥锁

一次只能一个线程拥有互斥锁,其他线程只有等待。

为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右。

实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)的效果可能不亚于使用自旋锁。

条件变量

互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。

当条件变量不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。

一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。

自旋锁

如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。

自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。

83.delete和delete[]

  • delete只会调用一次析构函数。

  • delete[]会调用数组中每个元素的析构函数。

84.为什么不能把所有的函数写成内联函数

内联函数以代码复杂为代价,它以省去函数调用的开销来提高执行效率。

所以一方面如果内联函数体内代码执行时间相比函数调用开销较大,则没有太大的意义;

另一方面每一处内联函数的调用都要复制代码,消耗更多的内存空间,因此以下情况不宜使用内联函数:

  • 函数体内的代码比较长,将导致内存消耗代价

  • 函数体内有循环,函数执行时间要比函数调用开销大

85.为什么C++没有垃圾回收机制

实现一个垃圾回收器会带来额外的空间和时间开销。你需要开辟一定的空间保存指针的引用计数和对他们进行标记mark。然后需要单独开辟一个线程在空闲的时候进行free操作。

垃圾回收会使得C++不适合进行很多底层的操作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值