const 关键字的作用
- 作用:
- 避免修改。
- 避免多次分配内存。
- 类型检查、作用域检查。
- 定义变量
- 修饰函数参数
- 修饰函数返回值
- 类中常成员函数
static 关键字的作用
- 修饰的对象
- 使用场景
- 函数体中:使用
static
修饰变量时,该变量的内存只会被分配一次,同时该变量的访问范围只在该函数体内。 - 模块中:
- 在源文件(
.c
/ .cpp
)中使用 static
修饰变量或函数,该变量或函数只能在该源文件中被访问。 - 在头文件(
.h
/ .hpp
)中使用 static
修饰变量或函数,所有包含该头文件的源文件都可以访问该变量或函数。 - 主要作用是避免命名重复,因为在多个模块中使用
static
修饰相同名称的变量,不会发生冲突。
- 类中
- 使用
static
修饰的成员变量或成员函数,其属于整个类。 - 静态成员函数不接收
this
指针,只能访问静态成员变量。
指针和引用的作用以及区别
- 区别
- 是否需要初始化
- 指针不需要初始化,但是最好初始化,避免野指针。
- 引用必须要初始化,并且不能初始化为空对象,初始化后不能被改变。
- 是否允许为空
- 是否直接操作对象
- 指针通过某个指针变量指向一个对象,对它所指向的变量间接操作。
- 引用是目标对象的别名,对引用操作就是直接对目标对象操作。
- 是否是对象
- 指针是对象,指针是有地址的,可以定义指针的指针。
- 引用不是对象,没有实际地址,不能定义引用的指针,也不能定义引用的引用。
- 作用
- 引用的作用:
- 传参时避免内存分配、对象数据的复制。
- 作为函数返回值,避免对象数据的复制。
- 指针的作用
- 传参时仅仅分配指针的内存空间,避免对象数据的复制。
- 实现多态的方法之一(基类指针指向子类具体对象)。
如何避免野指针
- 什么是野指针,使用后有什么问题?
- 如果某指针没有初始化,那么系统会为其随机分配一个地址。
- 如果使用野指针,容易造成内存泄漏,出现段错误。
- 如何避免?
- 指针没有指向具体对象时,要初始化为空。
- 使用指针操作指针指向的对象时,要检查是否为指针对象分配内存。
- 使用
new
、malloc
分配空间后,需要检查指针是否为空,如果为空,说明分配内存失败,这时需要强制退出。 - 在 C 语言中,使用
malloc
之后,需要将空间数据置空(memset
)。 - 释放内存后,需要将指针置空。
malloc、free 和 new、delete 的区别
- 定义
malloc
、free
是 C 语言中的库函数,不可以被重载。new
、delete
是 C++ 中的操作符,可以被重载。
- 使用方式
new
会自动计算所需分配的内存;malloc
需要手动计算所需分配的内存。new
返回的是对象类型的指针;malloc
返回的是 void*
,使用时需要转换为所需的类型。delete
释放内存时需要对象类型的指针;free
释放内存时需要 void*
类型的指针。new
分配失败会抛出异常;malloc
分配失败会返回 null
。new
在自由存储区(free store
)上分配内存;malloc
在堆上分配内存。
free store
是一个 C++ 抽象的概念:new
是一个操作符,在重载操作符的过程中,内部可以用 malloc
来分配内存,也可以通过内存池在静态全局区分配内存,所以 free store
既可以在堆上分配内存,也可以在静态全局区分配内存。
- new 先调用
operator new
函数,申请足够的内存;然后调用构造函数来初始化成员变量;最后返回该对象类型的指针。 delete
先调用析构函数,再调用 operator delete
函数释放内存。malloc
和 free
则是直接申请内存,释放内存。delete
、free
调用后,内存不会立刻被释放,指针也不会指向空,为了避免野指针,释放内存后(只是告诉操作系统:我们不使用这块内存了,你可以给其它地方使用),应该把指针指向空。
#include <iostream>
#include <new>
class SimpleMemoryPool {
public:
static SimpleMemoryPool& getInstance() {
static SimpleMemoryPool instance;
return instance;
}
void* allocate(std::size_t size) {
if (freeList.empty()) {
std::cerr << "Memory pool exhausted." << std::endl;
return nullptr;
}
void* ptr = freeList.top();
freeList.pop();
return ptr;
}
void deallocate(void* ptr) {
freeList.push(reinterpret_cast<char*>(ptr));
}
private:
std::stack<char*> freeList;
SimpleMemoryPool() {
for (int i = 0; i < 10; ++i) {
freeList.push(new char[1024]);
}
}
~SimpleMemoryPool() {
while (!freeList.empty()) {
delete[] freeList.top();
freeList.pop();
}
}
};
void* operator new(std::size_t size) {
void* ptr = SimpleMemoryPool::getInstance().allocate(size);
if (ptr) {
std::cout << "Custom operator new called. Allocated " << size << " bytes." << std::endl;
} else {
std::cerr << "Failed to allocate memory." << std::endl;
throw std::bad_alloc();
}
return ptr;
}
void operator delete(void* ptr) noexcept {
std::cout << "Custom operator delete called." << std::endl;
SimpleMemoryPool::getInstance().deallocate(ptr);
}
int main() {
int* p = new int;
delete p;
return 0;
}
extern 关键字作用
- 原理:引用还没有声明的变量或者函数,这个变量或者函数在其它地方声明了(该变量或函数不能是静态的)。
- 使用场景:
- C++ 调用 C 语言编译的变量或者函数。
- C++ 支持函数重载,在编译过程中会对变量或者函数进行重命名 — 在变量名或者函数名前加一些特殊字符,通过这种方式支持重载;而 C 语言重命名比较简单。
- 因为 C++ 和 C 语言在编译过程中对变量或者函数的命名方式不一样,所以需要告诉编译器我们现在要访问的全局变量以及函数是以 C 语言进行编译的,那么就需要使用
extern "C"
。
gcc extern_c.c -c
gcc extern_cpp.cpp -c
gcc extern_c.o extern_cpp.o -o extern_test
#include <stdio.h>
int var_c = 99;
void func_c() {
printf("func_c var_c = %d\n", var_c);
}
#include "stdio.h"
extern "C" int var_c;
extern "C" void func_c();
int main() {
func_c();
var_c = 100;
printf("extern_cpp var_c = %d \n", var_c);
}
- 都是 C 语言或者都是 C++。
- 使用文件内部还没有声明的变量或者函数。
#include <iostream>
int main() {
extern int var_main;
extern void func_main();
func_main();
std::cout << "main var_main = " << var_main << std::endl;
return 0;
}
int var_main = 100;
void func_main() {
std::cout << "func_main var_main = " << var_main << std::endl;
}
- 使用外部文件声明的变量或者函数。
g++ extern_main.cpp extern_other.cpp -o extern_test
#include <iostream>
int var_other = 99;
void func_other() {
std::cout << "func_other var_other = " << var_other << std::endl;
}
#include <iostream>
int main() {
extern int var_other;
extern void func_other();
func_other();
std::cout << "main var_main = " << var_other << std::endl;
return 0;
}
strcpy、sprintf、memcpy 的区别
- 相同点:都可以实现字符串拷贝(开发会使用
strcpy
进行字符串拷贝)。 - 不同点:
- 实现功能
strcpy
实现字符串拷贝,遇到 \0
结束(会把 \0
拷贝过去)。sprintf
主要用来格式化字符串。memcpy
实现内存块拷贝,根据 size
大小进行复制。
#include <stdio.h>
#include <string.h>
int main() {
char buf_a[64] = {'z', 'z', 'z', 'z', 'z'};
char buf_b[64] = {'a', 'b', 'c', '\0', 'd'};
char * str = strcpy(buf_a, buf_b);
printf("str = %s, buf_a = %s \n", str, buf_a);
int n = sprintf(buf_a, "len: %d, str: %s, addr:%p", 6, buf_b, buf_b);
printf("n = %d, buf_a = %s \n", n, buf_a);
char buf_c[64] = {'z', 'z', 'z', 'z', 'z'};
char buf_d[64] = {'a', 'b', 'c', '\0', 'd'};
memcpy(buf_c, buf_d, 3);
printf("buf_a : %s \n", buf_c);
memcpy(buf_c, buf_d, 4);
printf("buf_a : %s \n", buf_c);
return 0;
}
- 执行效率:
memcpy
最快,strcpy
次之(因为要检查 \0
),sprintf
最慢(因为要解析格式化字符串,还可能要进行数据类型转换)。
- 操作对象
strcpy
操作对象为字符串。sprintf
操作对象可以为多种数据类型。memcpy
操作的是内存地址。
- 头文件
sprintf
头文件为 stdio.h
。strcpy
和 memcpy
头文件为 string.h
。
C/C++ 中类型转换以及使用场景
- C 语言中的类型转换:
(T) exp
T(exp)
- 弊端:如果转换出错,未来复现 bug 的时候,很难找到类型转换的位置。
- C++ 中的类型转换:
static_cast<T>(exp)
(编译期行为)
- 类层次间转换。
- 上行转换是安全的。(子类指针转换为父类指针)
- 下行转换是不安全的,没有动态类型检查。(父类指针转换为子类指针)
- 基本类型转换。
- 空指针转换为目标类型的空指针。
non-const
转换为 const
(反之不行)。- 局限性:不能去掉
const
、volitale
等属性。
const_cast<T>(exp)
(编译期行为)
- 去掉对象指针或对象引用的
const
属性。 - 目的:修改指针或引用的权限,可以通过指针或引用修改某块内存的值。
dynamic_cast<T>(exp)
(运行时行为)
- 用于多态,在运行时进行类型转换,有动态类型检查,是安全的。
- 在一个类层次结构中安全地类型转换,把基类指针或引用转换为派生类指针或引用。(下行转换)
- 因为不存在空引用,所以转换失败会抛出
bad_cast
异常。 - 因为存在空指针,所以转换失败会返回
0
。
reinterpret_cast<T>(exp)
(编译期行为)
- 改变指针或引用的类型。
- 将指针或引用转换为一个整型。
- 将整型转换为指针或引用。
T
必须为指针、引用、整型、函数指针、成员指针(this
指针)。- 仅仅是比特位的拷贝,没有安全检查。
- 使用场景:
- 基本类型转换用
static_cast
。 - 去掉
const
属性用 const_cast
。 - 多态类之间的类型转换用
dynamic_cast
。 - 不同类型的指针类型转换使用
reinterpret_cast
。
构造函数有哪些,以及调用时机
#include <iostream>
using namespace std;
class T {
public:
T() {
cout << "构造函数 T(): " << this << endl;
}
~T() {
cout << "析构函数 ~T(): " << this << endl;
}
T(const T&) {
cout << "T(const T&) 拷贝构造: " << this << endl;
}
T& operator=(const T&) {
cout << "T& operator=(const T&) 拷贝赋值构造: " << this << endl;
}
T(T &&) {
cout << "T(T &&) 移动构造: " << this << endl;
}
T& operator=(T &&) {
cout << "T& operator=(T &&) 移动赋值构造: " << this << endl;
}
};
T CreateT() {
T temp;
return temp;
}
int main() {
{
T t1;
T t2 = t1;
T t3(t1);
T t4{t1};
}
{
T t1;
T t2;
t1 = t2;
}
{
cout << "=======" << endl;
T t = CreateT();
T t1;
T t2(std::move(t1));
cout << "=======" << endl;
}
{
T t;
t = T();
T t1, t2;
t1 = std::move(t2);
}
return 0;
}
C++ 什么时候生成默认构造函数
- 空的类定义,不会生成默认构造函数,没有意义。即便里面包含成员变量,如果成员变量是基础类型,还是不会生成默认构造函数,因为编译器也不知道用什么值去初始化。
- 什么情况下会生成默认构造函数?
- 类
A
内数据成员是对象 B
,而类 B
提供了默认构造函数。
- 为了让
B
的构造函数能被调用到,不得不为 A
生成默认构造函数。
- 类的基类提供了默认构造函数。
- 子类构造函数要先初始化父类,再初始化自身成员变量。
- 如果父类没有提供默认构造函数,子类也无需提供默认构造函数。
- 如果父类提供了默认构造函数,子类不得不生成默认构造函数。
- 类内定义了虚函数。
- 为了实现多态机制,需要为类维护一个虚函数表。
- 类的所有对象都需要保存一个指向该虚函数表的指针。
- 因为对象需要初始化指向该虚函数表的指针,所以不得不提供默认构造函数来初始化虚函数表指针。
- 类使用了虚继承。
- 虚基类表记录了虚基类与本类的偏移地址。
- 为了实现虚继承,对象在初始化阶段需要维护一个指向虚基类表的指针。
- 因为对象需要初始化指向该虚基类表的指针,所以不得不提供默认构造函数来初始化虚基类表指针。
C++ 什么时候生成默认拷贝构造函数
- 如果不提供默认拷贝构造函数,那么会按照位拷贝(一个字节一个字节地拷贝)进行拷贝,有些时候位拷贝会出现不是我们所预期的行为。
A a = b;
- 什么时候必须生成默认拷贝构造函数?
- 类成员变量也是一个类,该成员类有默认拷贝构造函数。
- 为了让成员类的默认拷贝构造函数能够被调用到,不得不为类生成默认的拷贝构造函数。
- 类继承自一个基类,这个基类有默认拷贝构造函数。
- 子类执行拷贝构造函数的时候,先调用父类的拷贝构造函数。
- 为了能调用到父类的拷贝构造函数,子类不得不生成默认的拷贝构造函数。
- 类成员中包含一个或多个虚函数。
- 为了实现多态机制,需要为类维护一个虚函数表。
- 类所有对象都需要保存一个指向该虚函数表的指针。
- 如果不提供默认拷贝构造函数,会进行一个位拷贝,那么虚函数表指针可能会丢失。
- 本类的虚函数表指针被覆盖掉。
- 如果提供了拷贝构造函数,编译器会为我们初始化虚函数表指针。
- 所以不得不为类生成默认拷贝构造函数,完成虚函数表指针的拷贝。
- 类继承自一个基类,这个基类有一个或者多个虚函数。
- 如果不提供默认构造函数,会进行位拷贝,那么基类拷贝构造函数就不能被调用,从而虚函数表指针可能会丢失。
- 所以不得不为类生成默认拷贝构造函数,以此完成基类拷贝构造函数的调用,从而完成虚函数表指针的拷贝。
C++ 什么是深拷贝、浅拷贝
- 拷贝情况:
- 用同类的对象构建一个新的类对象。
A a1;
A a2(a1);
- 函数传参为类对象,值传递,类的复制。
- 函数返回值是类对象。
- 运算
- 浅拷贝
- 深拷贝
- 为对象中的动态成员(指针)重新动态分配空间,或者重新分配资源。
- 重写拷贝构造函数,重载 = 运算符。
继承下的构造函数和析构函数执行顺序
- 继承下,构造函数按照依赖链,从上往下进行构造;析构函数按照依赖链,从下往上进行析构。
- 单继承:
- 多继承。
- 成员类按照顺序构造,按照相反顺序析构。
- 类的构造依赖成员类的构造,基类比成员类依赖性更强。
- 多继承中基类按照声明顺序构造,按照相反顺序析构。
class MBase1 {
public:
MBase1() {
cout << "MBase1 construction" << endl;
}
~MBase1() {
cout << "~MBase1 destruction" << endl;
}
};
class MBase2 {
public:
MBase2() {
cout << "MBase2 construction" << endl;
}
~MBase2() {
cout << "~MBase2 destruction" << endl;
}
};
class MDrive1 {
public:
MDrive1() {
cout << "MDrive1 construction" << endl;
}
~MDrive1() {
cout << "~MDrive1 destruction" << endl;
}
};
class MDrive2 {
public:
MDrive2() {
cout << "MDrive2 construction" << endl;
}
~MDrive2() {
cout << "~MDrive2 destruction" << endl;
}
};
class MDrive : public MBase1, public MBase2 {
public:
MDrive() {
cout << "MDrive construction" << endl;
}
~MDrive() {
cout << "~MDrive destruction" << endl;
}
private:
MDrive1 md1;
MDrive2 md2;
};
int main () {
MDrive md;
return 0;
}
动态库和静态库的区别
- 生成方式:
- 链接方式:
- 静态链接:把静态库编译进目标文件。
- 动态链接:
- 没有把库编译进目标文件。
- 程序运行时才去加载运行代码。
- 地址无关代码技术
-fPIC
。 - 装载时重定位,定位到
LD_LIBRARY_PATH
。
- 链接时只会做语法检查。
- 空间占用:
- 如果多个应用程序都使用到了同一个静态库, 那么该静态库会存在多个副本,内存和磁盘都有多份。
- 动态库则只有一个副本。
- 使用方式:
- 静态库所在程序可以直接运行。
- 动态库所在程序需要动态加载,程序的运行环境需要指定查找路径
LD_LIBRARY_PATH
。
- 执行速度:
- 库文件发生变更:
- 接口改变:静态库和可执行程序、动态库和可执行程序都需要重新编译。
- 接口实现改变:静态库和可执行程序需要重新编译;仅仅是动态库需要重新编译。
int main() {
cout << add(1, 2) << endl;
cout << del(1, 2) << endl;
return 0;
}