跳到主要内容

补充知识点

注意

这里是个人过去遗漏的内容,不系统且不完整,请谨慎参考;

1、宏定义和typedef?

  • 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
  • 宏替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分。

宏对比函数调用,同理,宏仅仅是文本替换,不存在调用,返回值,安全检测等操作。

  • 宏不检查类型;typedef会检查数据类型。
  • 宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。
  • 注意对指针的操作,typedef char * p_char和#define p_char char *区别巨大。

2、C++中struct和class的区别

相同点

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

不同点

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

扩展:C++和C的struct区别

  • C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)
  • C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数
  • C++中,struct增加了访问权限,且可以和类一样有成员函数,成员默认访问说明符为public(为了与C兼容)
  • struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class;);C++中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例

3、C++的顶层const和底层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,b3是常量不可变
const int* const b4 = &a; //前一个const为底层,后一个为顶层,b4不可变
const int& b5 = a; //用于声明引用变量,都是底层const

区分作用

  • 执行对象拷贝时有限制,常量的底层const不能赋值给非常量的底层const
  • 使用命名的强制类型转换函数const_cast时,只能改变运算对象的底层const
const int a;
int const a;
const int *a;
int *const a;
  • int const a和const int a均表示定义常量类型a。
  • const int *a,其中a为指向int型变量的指针,const在 * 左侧,表示a指向不可变常量。(看成const (*a),对引用加const)
  • int *const a,依旧是指针类型,表示a为指向整型数据的常指针。(看成const(a),对指针const)

4、C++中const和static对比

static

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

const

  • 不考虑类的情况
    • const常量在定义时必须初始化,之后无法更改
    • const形参可以接收const和非const类型的实参
  • 考虑类的情况
    • const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化
    • const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值 补充一点const相关:const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突。

5、数组名和指针(这里为指向数组首元素的指针)区别?

  • 二者均可通过增减偏移量来访问数组中的元素。

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

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

6、拷贝初始化和直接初始化

  • 当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。举例如下
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 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数
  • **为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就完全等价于直接初始化了(语句1和语句3等价),但是需要辨别两种情况。
    • 当拷贝构造函数为private时:语句3和语句4在编译时会报错
    • 使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译时会报错

7、C++有哪几种的构造函数

  • 默认构造函数(Default Constructor) 默认构造函数是没有参数的构造函数。当对象创建时,如果没有提供初始化参数,就会调用默认构造函数。编译器会自动生成一个默认构造函数,如果类中没有定义任何构造函数。
class MyClass {
public:
MyClass() { // 默认构造函数
// 初始化代码
}
};
  • 参数化构造函数(Parameterized Constructor) 也称为初始化构造函数,是指带有参数的构造函数。允许在创建对象时传递参数,以初始化成员变量。
class MyClass {
public:
MyClass(int x, int y) { // 参数化构造函数
// 使用参数初始化对象
}
};
  • 拷贝构造函数(Copy Constructor) 拷贝构造函数用于创建一个新对象,使其成为已有对象的副本。它的参数通常是该类对象的 const 引用。
class MyClass {
public:
MyClass(const MyClass &obj) { // 拷贝构造函数
// 使用 obj 初始化新对象
}
};
  • 移动构造函数(Move Constructor) 移动构造函数通过“移动”资源,而不是“复制”资源来初始化新对象。它的参数是该类对象的右值引用,常用于优化程序性能,特别是在涉及大量资源管理时。
class MyClass {
public:
MyClass(MyClass &&obj) noexcept { // 移动构造函数
// 移动 obj 的资源到新对象
}
};
  • 委托构造函数(Delegating Constructor) 委托构造函数是在一个构造函数中调用同一个类中的另一个构造函数。这样可以减少代码重复,并使构造函数逻辑集中。
class MyClass {
public:
MyClass() : MyClass(0, 0) { // 委托构造函数
// 使用参数化构造函数进行初始化
}

MyClass(int x, int y) {
// 初始化代码
}
};
  • 转换构造函数(Conversion Constructor) 转换构造函数是指只有一个参数且未被标记为 explicit 的构造函数。它允许通过该参数的类型隐式地转换为类对象。
class MyClass {
public:
MyClass(int x) { // 转换构造函数
// 允许从 int 隐式转换为 MyClass 对象
}
};

8、内联函数

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

内联函数适用场景:

  • 使用宏定义的地方都可以使用 inline 函数。
  • 作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率。

9、什么情况下会调用拷贝构造函数

  • 用类的一个实例化对象去初始化另一个对象的时候
  • 函数的参数是类的对象时(非引用传递)
  • 函数的返回值是函数体内局部对象的类的对象时 ,此时虽然发生(Named return Value优化)NRV优化,但是由于返回方式是值传递,所以会在返回值的地方调用拷贝构造函数

第三种情况在Linux g++ 下则不会发生拷贝构造函数,不仅如此即使返回局部对象的引用,依然不会发生拷贝构造函数

10、C++中有几种类型的new(独立文章)

在C++中,new有三种典型的使用方法:plain new,nothrow new和placement new

11、值传递、指针传递、引用传递的区别和效率

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

  2. 指针传递:同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个固定为4字节的地址。(传值,传递的是地址值)

  3. 引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名。(传地址)

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

12、从汇编层去解释一下引用

9:      int x = 1;

00401048 mov dword ptr [ebp-4],1

10: int &b = x;

0040104F lea eax,[ebp-4]

00401052 mov dword ptr [ebp-8],eax

x的地址为ebp-4,b的地址为ebp-8,因为栈内的变量内存是从高往低进行分配的,所以b的地址比x的低。 lea eax,[ebp-4] 这条语句将x的地址ebp-4放入eax寄存器 mov dword ptr [ebp-8],eax 这条语句将eax的值放入b的地址

ebp-8中上面两条汇编的作用即:将x的地址存入变量b中,这不和将某个变量的地址存入指针变量是一样的吗?所以从汇编层次来看,的确引用是通过指针来实现的。

16、对象复用的了解,零拷贝的了解

对象复用

对象复用其本质是一种设计模式:Flyweight享元模式。

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

零拷贝

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

零拷贝技术可以减少数据拷贝和共享总线操作的次数。

在C++中,vector的一个成员函数**emplace_back()**很好地体现了零拷贝技术,它跟push_back()函数一样可以将一个元素插入容器尾部,区别在于:使用push_back()函数需要调用拷贝构造函数和转移构造函数,而使用emplace_back()插入的元素原地构造,不需要触发拷贝构造和转移构造,效率更高。

17、cout和printf有什么区别?

很多人认为cout是一个函数,其实不是的,它是类std::ostream的全局对象。

cout后可以跟不同的类型是因为cout已存在针对各种类型数据的重载,所以会自动识别数据的类型。

输出过程会首先将输出字符放入缓冲区,然后输出到屏幕。

cout是有缓冲输出:

cout < < "abc " < <endl; 
cout < < "abc\n ";
cout < <flush; //这两个才是一样的.

flush立即强迫缓冲输出。

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

18、说一说strcpy、sprintf与memcpy这三个函数的不同之处

  • 操作对象不同

    • strcpy的两个操作对象均为字符串

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

    • memcpy的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。

  • 执行效率不同

    • memcpy最高,strcpy次之,sprintf的效率最低。
  • 实现功能不同

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

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

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

19. 内存分区

在大多数现代操作系统中,程序的内存通常分为以下几个主要区域:

  • 代码区(Text Segment):存放程序的可执行代码,也就是编译后的指令。这个区域通常是只读的,以防止程序意外修改自己的指令。

  • 数据区(Data Segment)

    • 全局区(Data Segment - Initialized Data):存放已初始化的全局变量和静态变量。
    • BSS段(Uninitialized Data Segment - BSS):存放未初始化的全局变量和静态变量。在程序运行前,这部分数据会被系统初始化为零。
  • 堆区(Heap):用于动态分配的内存,典型的分配方式是通过mallocnew等函数。程序员需要手动管理这些内存,负责分配和释放。

  • 栈区(Stack):用于函数调用时分配局部变量、参数、返回地址等信息。栈区内存由编译器自动分配和释放,通常随着函数的进入和退出进行分配和回收。

  • 常量区(Constant Segment):存放常量数据,例如字符串常量、const修饰的全局变量等。这部分数据通常是只读的。

20、malloc、calloc、realloc和alloca

1、malloc:不初始化。

void* malloc(size_t size);
// 分配一块size大小的内存,位于堆。

2、calloc:初始化为0。

void* calloc(size_t num, size_t size);
// 分配一块内存,大小包含num个大小为size的元素;

3、realloc:内存块原来的内容不变,如果内存变小,则相应内容少掉。

void* realloc(void* ptr , size_t size);
// 改变ptr指向的内存为size;

4、alloca:在栈上分配空间,作用域结束自动释放。

void* alloca(size_t size);
// 分配一块内存,大小为size,位于栈中。