0%

  1. 栈区(Stack)
    • 栈区用于存储函数的局部变量、函数参数、函数调用时的返回地址以及函数调用过程中的上下文信息。
    • 栈区是由编译器自动管理的,它的大小和生命周期都是由程序的函数调用情况决定的。
    • 栈区中的数据是连续存储的,变量的内存分配是按照后进先出(LIFO)的原则进行的。
  2. 堆区(Heap)
    • 堆区用于存储程序动态分配的内存,即通过 newmalloc 等运算符或函数进行的内存分配。
    • 堆区的内存空间通常由程序员手动管理,需要在不需要使用时手动释放,否则可能会导致内存泄漏。
    • 堆区的数据是不连续存储的,变量的内存分配是动态的,可以根据程序需要进行分配和释放。
  3. 全局区/静态区(Static/Global)
    • 全局区用于存储全局变量、静态变量、常量和字符串常量。
    • 全局区在程序启动时就会被分配,并在程序结束时释放,它的生命周期与整个程序的生命周期相同。
    • 全局区中的数据是静态分配的,变量的内存分配是固定的,不会随着函数的调用而改变。
  4. 常量区(Constants)
    • 常量区用于存储程序中的常量,例如字符串常量。
    • 常量区的数据是只读的,不能被修改。
    • 常量区通常是放在静态存储区域中的,因此也被称为静态常量区。
  5. 代码区(Code)
    • 代码区用于存储程序的可执行代码,包括函数的机器代码和指令。
    • 代码区通常是只读的,不能被修改。
    • 代码区在程序启动时就会被加载到内存中,因此也被称为只读存储区。

异常处理是一种在程序执行期间处理错误或异常情况的机制,它允许程序在发生错误时转移到错误处理代码,从而提高程序的健壮性和可靠性。在 C++ 中,异常处理通过 trythrowcatch 关键字来实现。

  1. try: 用于包含可能会引发异常的代码。程序在执行 try 块中的代码时,如果发生异常,将会跳转到匹配的 catch 块进行异常处理。try 块必须和至少一个 catch 块配对使用。
  2. throw: 用于在程序中手动抛出异常。它可以抛出任何类型的异常,包括基本数据类型、类对象、指针等。当程序执行到 throw 语句时,会立即跳转到匹配的 catch 块中,并执行相应的异常处理代码。
  3. catch:用于捕获并处理 try 块中抛出的异常。catch 块必须紧跟在 try 块之后,用于处理特定类型的异常或者所有类型的异常。catch 块中可以使用异常类型来捕获特定类型的异常,也可以使用省略号 ... 来捕获所有类型的异常。一个 try 块可以有多个 catch 块,每个 catch 块可以捕获不同类型的异常,但只有第一个匹配的 catch 块会被执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>

void processNumber(int num) {
if (num == 0) {
throw "Divide by zero exception";
} else if (num < 0) {
throw std::runtime_error("Negative number exception");
} else {
std::cout << "Result: " << 100 / num << std::endl;
}
}

int main() {
try {
processNumber(0);
} catch (const char* msg) {//捕获自定义的异常
std::cerr << "Caught exception: " << msg << std::endl;
} catch (const std::exception& e) {//捕获到任何继承自std::exception的异常,并使用 e.what()方法获取异常对象的描述信息
std::cerr << "Caught exception: " << e.what() << std::endl;
}

try {
processNumber(-5);
} catch (const char* msg) {
std::cerr << "Caught exception: " << msg << std::endl;
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}

try {
processNumber(10);
} catch (const char* msg) {
std::cerr << "Caught exception: " << msg << std::endl;
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}

return 0;
}

//运行结果
//Caught exception: Divide by zero exception
//Caught exception: Negative number exception
//Result: 10

  • auto: 用于自动类型推断,让编译器根据变量的初始化表达式推导出其类型。
  • const: 声明常量,使得变量的数值在初始化后不能被修改。
  • constexpr: 用于声明常量表达式,使得函数或变量在编译期间就能被计算出来。
1
2
3
4
5
6
7
8
9
10
11
12
constexpr int size = 10; // 声明常量

constexpr int square(int x) { // 声明函数
return x * x;
}

class MyClass { // 声明类的构造函数,在编译期间初始化对象
public:
constexpr MyClass(int x) : data(x) {}
private:
int data;
};
  • static: 声明静态变量或函数,使得它们在程序的整个生命周期内保持其存在,即使超出了其定义的作用域。
  • volatile: 告知编译器,某个变量的值可能会在程序的执行过程中被意外地改变,应该禁止对其进行某些优化。
  • extern: 声明一个变量或函数是在其他文件中定义的,使得在当前文件中可以引用它。
  • mutable: 用于声明类的成员变量,在const成员函数中允许修改这些变量。
1
2
3
4
5
6
7
8
9
10
11
class person
{
int m_A;
mutable int m_B;//特殊变量 在常函数里值也可以被修改
public:
void add() const//在函数里不可修改this指针指向的值 常量指针
{
m_A = 10;//错误 不可修改值,this已经被修饰为常量指针
m_B = 20;//正确
}
};
  • inline: 声明函数为内联函数,建议编译器在调用时将函数体直接插入到调用处,以减少函数调用的开销。
1
2
3
4
5
6
7
inline int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 4);
return 0;
}
  • explicit : 用于修饰单参数的构造函数,防止隐式类型转换发生。
1
2
3
4
5
6
7
8
9
10
11
class MyClass {
public:
explicit MyClass(int x) : data(x) {}
private:
int data;
};
int main() {
MyClass obj = 10; // 错误,因为构造函数是explicit的
MyClass obj2(10); // 正确
return 0;
}
  • virtual: 声明虚函数,用于实现多态性,在派生类中可以重写基类的虚函数。
  • override: 用于在派生类中标记重写基类的虚函数。
1
2
3
4
5
6
7
8
9
10
11
12
class A{
public:
virtual void foo(){
cout << "father" << endl;
};
}
class B : public A{
public:
void foo() override{
cout << "son override" << endl;
};
}
  • final: 用于标记类不能被继承,或者虚函数不能被重写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base{
public:
virtual void foo();
};

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

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

class C : B // 错误
{
};
  • nullptr: 表示空指针常量。
  • typename: 用于告知编译器某个名称是类型而不是变量。
  • try/catch/throw: 用于异常处理的关键字。
  • namespace: 声明命名空间,用于避免命名冲突。

一、定义

宏定义#define是用来进行文本替换的预处理指令。

  1. 宏名称:宏定义的名称可以是任何有效的标识符,类似于变量名或函数名,但通常会使用大写字母来表示,以便于与变量名或函数名区分开来。
  2. 替换文本:替换文本可以是任何合法的C、C++代码片段,可以是一个表达式、一个语句、甚至是一段复杂的代码块。替换文本中可以包含其他已经定义过的宏,也可以包含预处理运算符(如 ### 等)来进行字符串化或拼接等操作。

宏定义的几个常见应用包括:

  • 定义常量:通过宏定义可以方便地定义常量,如 #define PI 3.14159
  • 简化代码:通过宏定义可以将一些重复的代码片段定义为宏,以减少代码量,提高可读性,比如 #define MAX(x, y) ((x) > (y) ? (x) : (y)) 用来求两个数中的最大值。
  • 条件编译:宏定义还可以用于条件编译,通过定义或取消定义不同的宏,可以控制编译器在不同条件下编译不同的代码片段,实现跨平台编译等功能。

二、#define和const的区别

  • define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用。
  • 安全性
    • define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错。
    • const常量有数据类型,编译器可以对其进行类型安全检查。
  • 内存占用
    • define只是将宏名称进行替换,在内存中会产生多分相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表
    • 宏替换发生在编译阶段之前,属于文本插入替换; const作用发生于编译过程中。
    • 宏不检查类型,const会检查数据类型。
    • 宏定义的数据没有分配内存空间,只是插入替换掉; const定义的变量只是值不能改变,但要分配内存空间。

三、#define和函数的区别

  • 编译过程
    • 宏在预处理阶段完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数。
  • 安全性
    • 宏定义中替换的函数不会进行参数类型检查,因此可能会导致参数类型不匹配或难以发现的错误。
    • 代码中写的函数在编译时期会进行类型检查,可以帮助检测到参数类型不匹配等错误。
  • 作用域
    • 宏定义中的替换文本会直接插入到代码中,不会创建新的作用域。因此,在宏定义中定义的变量或函数名可能会与代码中的其他变量或函数名冲突。
    • 代码中写的函数会创建一个新的作用域,函数内部的变量和函数名不会与外部的变量和函数名冲突。

四、#define和typedef的区别

  • 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
  • 宏替换发生在编译阶段之前,属于文本插入替换; typedef是编译的一部分。
  • 宏不检查类型; typedef会检查数据类型。
  • 宏不是语句,不在在最后加分号; typedef是语句,要加分号标识结束。

  1. sizeof是运算符,在编译时便得到结果;strlen是<string.h>库里的函数。
  2. strlen 函数用于计算以空字符 \0 结尾的字符串的长度,即字符串中的字符数(不包括空字符 \0

sizeof 操作符用于获取数据类型或对象在内存中的大小,单位为字节,对于数组,返回的是整个数组的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>

int main(){
const char* tmp = "stars";
char str[] = "stars";
char arr[10] = "spiderman";

printf("%d\n", sizeof(tmp));//8,计算的是char*的大小,64为系统中char*为8
printf("%d\n", strlen(tmp));//4,字符数

printf("%d\n", sizeof(str));//5,计算的是数组大小,即“stars\0”共五个元素,每个元素占1字节
printf("%d\n", strlen(str));//4,字符数

printf("%d\n", sizeof(arr));//10,数组大小1*10
printf("%d\n", strlen(arr));//9,字符数
return 0;
}

一、区别

  1. 类型安全性:
    • new/delete 是 C++ 中的操作符,它们能够调用对象的构造函数和析构函数,并且在动态分配内存时会自动计算所需的空间大小,因此是类型安全的。
    • malloc/free 是 C 语言中的函数,它们只是分配和释放内存块,不会调用任何构造函数或析构函数,也不会进行类型检查。因此,在 C++ 中使用 malloc/free 可能会导致内存泄漏或未定义的行为。
  2. 大小指定:
    • new 操作符根据指定的类型自动计算所需的内存大小,因此不需要显式指定分配的大小。
    • malloc 函数需要显式指定要分配的内存块的大小,它接受一个参数来指定所需的字节数。
  3. 返回类型:
    • new 操作符返回一个指向动态分配的对象的指针,类型为所分配对象的类型
    • malloc 函数返回一个指向动态分配的内存块的指针,类型为 void**需要进行显式的类型转换**。
  4. 构造和析构函数的调用:
    • new 操作符在分配内存后会调用对象的构造函数,用于初始化对象。
    • delete 操作符在释放内存前会调用对象的析构函数,用于清理对象。
    • malloc/free 不会调用任何构造或析构函数,它们只是分配和释放内存块。
  5. 异常处理:
    • new 操作符在分配失败时会抛出 std::bad_alloc 异常。
    • malloc 函数在分配失败时返回 NULL 指针,需要手动检查分配是否成功。
1
2
3
4
5
6
7
//动态分配了一个整数对象,并将其初始化为 5,然后将返回的指针赋值给指针变量 p1
int* p1 = new int(5);
delete p1;

//动态分配了一个大小为 sizeof(int) 字节的内存块,并将返回的指针强制转换为整型指针类型,然后将其赋值给指针变量 p2
int* p2 = (int*)malloc(sizeof(int));
free p2;

二、new和delete的实现原理

  1. 分配内存

​ 在使用 new 运算符来创建一个对象时,编译器会调用一个名为 operator new 的函数来分配内存。这个函数负责从堆中分配足够大小的内存来存储对象,并返回一个指向该内存的指针。如果分配失败,operator new 可能会抛出 std::bad_alloc 异常

  1. 构造对象

​ 分配内存后,编译器会调用对应的构造函数来初始化对象。这确保了对象的状态是有效的,并且任何必要的资源(如动态分配的内存或文件句柄)都已经被正确初始化。

  1. 返回指针

​ 一旦对象被正确地构造,new 运算符将返回一个指向新分配的内存的指针。这个指针可以用于访问对象的成员变量和方法。

  1. 释放内存

​ 当使用 delete 运算符来释放对象时,编译器会调用一个名为 operator delete 的函数。这个函数负责释放之前分配的内存,并且在必要时调用对象的析构函数来清理资源。

三、调用free会发生什么

  1. 标记内存块为可用: 调用 free 函数会将动态分配的内存块标记为可用。这意味着该内存块现在可以被系统重新分配给其他程序或进程使用。但是,该内存块的内容不会被清除或修改,因此在释放后的内存块中仍然可能包含之前存储的数据。
  2. 释放内存块: 被释放的内存块会被添加到系统的内存空闲列表中,以便以后的内存分配请求可以使用它。
  3. 合并相邻的空闲内存块: 在释放内存块后,系统可能会检查相邻的空闲内存块,并尝试将它们合并成一个更大的内存块。这有助于减少内存碎片化,并提高内存的利用率。
  4. 返回给操作系统(部分情况下): 在某些操作系统中,当大块内存被释放时,系统可能会将该内存块返回给操作系统,以便它可以被重新分配给其他程序或进程使用。但是,这并不是所有操作系统都会立即执行的操作,具体取决于操作系统的内存管理策略和实现。
悬空指针

当使用 delete 运算符释放动态分配的内存时,系统会回收该内存块,并将其标记为可用的空闲内存。然而,指向这块内存的指针本身并没有被修改,它仍然保持原来的值,但这个地址不再有效,这就是所谓的悬空指针。因此,为了避免使用悬空指针,通常将 ptr 设置为 nullptr

野指针

野指针是指向未知或无效内存地址的指针。野指针一般是未初始化的指针、已经被释放的指针或者指向无效内存的指针

  • 如果一个指针变量未被显式初始化,它将包含一个随机的值,这个值可能是一个无效的内存地址。当尝试访问这个指针所指向的内存时,就会产生未定义的行为。因此,为了防止出错,对于指针初始化时都是赋值为nullptr,这样在使用时编译器就不会直接报错,产生非法内存访问。
  • 当一个动态分配的内存块被释放(如使用 deletefree),但之后仍然有指针指向这块内存,这些指向已经释放的内存的指针就称为野指针。
  • 有时指针可能会指向已经被销毁或无效的对象,尝试访问这样的指针就会产生未定义的行为,这也属于野指针的范畴。

  1. 栈(Stack):
    • 栈用于存储函数的局部变量、函数参数以及函数调用所需要的上下文信息。
    • 栈内存由系统自动分配和释放,由编译器管理。当一个函数被调用时,其所需的局部变量和参数被存储在栈上,当函数执行完毕时,这些变量的内存空间会被自动释放。
    • 栈的大小有限,通常较小,它们的大小在程序运行时是固定的,栈顶和栈底是预设好的,栈向栈底扩展。
    • 栈上的内存分配和释放速度比较快,不会有碎片。
  2. 堆(Heap):
    • 堆是用于动态内存分配的区域,用于存储程序运行时需要动态分配的数据,例如使用 malloc()calloc()new 等函数动态分配的内存。
    • 堆内存的分配和释放由程序员手动管理。程序员需要显式地分配内存并在不需要使用时手动释放内存,否则会产生内存泄漏。
    • 堆的大小通常比栈大得多,大小受操作系统的限制,通常由操作系统的虚拟内存大小决定,堆向高地址扩展是不连续的内存区域,大小可灵活调整。
    • 由于堆上的内存分配是动态的,需要维护额外的信息来跟踪已分配和未分配的内存块,因此堆上的内存分配和释放速度相对较慢,会产生内存碎片。

在 C 语言中,结构体的对齐方式是编译器根据平台和编译器的要求进行的一种内存对齐操作,其目的是为了提高内存访问的效率和性能。对齐方式可以通过编译器的选项或者特定的编译指令来控制,但通常情况下编译器会使用默认的对齐方式。

遵循的一般对齐规则:

  • 每个成员变量的起始地址都必须是其数据类型大小的倍数,如果不能则在前一个成员后面补充字节,默认的对齐大小是4:这意味着一个 int 类型的变量通常会从内存地址为 0、4、8、12 等处开始,而一个 double 类型的变量通常会从内存地址为 0、8、16、24 等处开始。
  • 结构体的总大小必须是其最大成员变量大小的倍数

例如下面这个例子:

1
2
3
4
5
struct A{
int a;
double b;
char c;
};

假设a的起始地址是0x0000,则a所占的内存空间为0x00000x0003,下一个地址0x004不是double的大小8的整数倍,故补零到0x0007。则b的内存为0x00080x0010。下一个地址0x0011为c的地址。共8+8+1=17字节,但总大小不是最大数据类型大小8的整数倍,故总大小为24。

而下面的例子:

1
2
3
4
5
struct A{
char a;
int b;
double c;
};

假设a的地址:0x00000x0001,下一个地址0x0002不是int大小4的整数倍,故b的地址:0x00040x0007,c的地址:0x0008~0x0010,共4+4+8=16个字节,且是最大数据类型大小8的整数倍,故总大小为16。

Git是一个强大的版本控制系统,它用于跟踪文件的变化并协作开发代码。

  1. 创建新的仓库:开始使用Git,需要在项目文件夹中初始化一个新的Git仓库。进入到项目文件夹,然后

    1
    git init
  2. 将文件添加到仓库:在初始化了仓库之后,可以将项目文件添加到Git仓库中以进行版本控制。

    使用add将修改的文件添加到暂存区:

    1
    git add <file1> <file2> ...

    若是修改的文件较多,可直接使用

    1
    git add .
  3. 提交更改:文件添加到暂存区后,就可以使用commit将它们提交到仓库中。

    1
    git commit -m "Your commit message"

    可以在提交信息中清晰地描述你所做的更改。

  4. 查看状态:你可以使用以下命令来查看当前仓库的状态,包括已修改、已暂存和未跟踪的文件。

    1
    git status
  5. 查看提交历史

    1
    git log
  6. 比较文件差异:

    1
    git diff <file>

    显示特定文件在工作区和暂存区之间的差异。

    1
    git diff <commit id>

    显示当前工作目录中的文件与指定提交之间的差异

  7. 版本回退

    1
    git checkout <commit id>

    将工作区和暂存区恢复到指定提交的状态。

  8. 创建分支:分支允许你在不影响主线开发的情况下开发新功能或修复bug。

    1
    git branch <branch-name>
  9. 切换分支:你可以使用以下命令切换到其他分支。

    1
    git checkout <branch-name>
  10. 合并分支:当你完成了在某个分支上的工作,并且希望将其合并到主分支时,你可以使用以下命令。

    1
    2
    git checkout main
    git merge <branch-name>
  11. 远程仓库:Git还支持与远程仓库进行交互,例如GitHub、GitLab等。你可以使用以下命令将本地仓库与远程仓库关联。

    1
    git remote add origin <remote-repository-url>
  12. 推送到远程仓库:一旦你与远程仓库建立了关联,你可以将本地提交推送到远程仓库。

    1
    git push -u origin main

    一旦你将本地的提交推送到远程仓库,你可能会希望在本地仓库中获取远程仓库的最新内容,以便与其他团队成员的更改保持同步。你可以使用 git pull 命令来拉取远程仓库的内容。以下是步骤:

    1. 确保当前在正确的分支上:首先,确保你在想要拉取远程仓库内容的本地分支上。

      1
      git checkout main

      这里的 “main” 可以根据你的实际情况而定,确保你在想要拉取更新的目标分支上。

    2. 执行 git pull:运行 git pull 命令来拉取远程仓库的最新内容并将其合并到你当前所在的分支。

      1
      git pull

      这将获取远程仓库的最新提交,并尝试将其合并到你当前所在的分支中。

    如果你希望拉取特定远程仓库的特定分支的更新,你可以在 git pull 命令中提供远程仓库和分支的名称。例如,如果要从名为 “origin” 的远程仓库的 “main” 分支拉取更新,可以执行以下命令:

    1
    git pull origin main

    这将从远程仓库的 “main” 分支拉取更新,并将其合并到你当前所在的分支。

git checkout

git checkout 命令在 Git 中有多种用途,它可以用于切换分支、创建分支、恢复文件和检出提交等操作。以下是 git checkout 命令的几种常见用法:

  1. 切换分支

    1
    git checkout <branch-name>

    这会将当前工作目录切换到指定的分支。例如,要切换到名为 main 的分支,你可以运行:

    1
    git checkout main

    你也可以使用 git switch 命令来执行相同的操作。

  2. 创建新分支并切换到该分支

    1
    git checkout -b <new-branch-name>

    这会创建一个新的分支,并将当前工作目录切换到该分支。例如,要创建一个名为 feature 的新分支并切换到该分支,你可以运行:

    1
    git checkout -b feature
  3. 恢复文件到指定状态

    1
    git checkout <file>

    这会将指定文件恢复到最近一次提交时的状态。如果你在工作区中对文件做了修改,但想要撤销这些修改并恢复到最近一次提交时的状态,可以使用这个命令。例如:

    1
    git checkout index.html

    这会将 index.html 文件恢复到最近一次提交时的状态。

  4. 检出提交

    1
    git checkout <commit>

    这会将你的工作目录和暂存区都恢复到指定提交时的状态,并将 HEAD 指针移动到该提交。通常情况下,你不应该在已提交的代码上工作,因为这可能会导致丢失未提交的更改。然而,这个命令在需要检查历史提交的内容或创建分离头指针时很有用。如果你只是想查看历史提交的内容,建议使用 git show <commit> 命令,这样可以避免创建分离头指针。

工作区和暂存区
  1. 工作区(Working Directory)

    工作区指的是你正在工作的项目目录,其中包含了你编辑、添加和删除文件的所有内容。换句话说,工作区是你电脑上能够看到的项目目录,其中包含了项目的所有文件和子文件夹。当你修改项目中的文件时,这些修改只存在于工作区中,还没有被 Git 跟踪。

  2. 暂存区(Staging Area)

    暂存区是 Git 提供的一个中间区域,用于存储你想要提交的更改。当你对工作区中的文件做出修改并准备提交时,首先需要将这些修改添加到暂存区。在暂存区中,你可以检查你要提交的更改,并决定是否需要将它们提交到版本控制系统中。

    一旦你将更改添加到暂存区,Git 就会记录这些更改的状态,包括哪些文件已经被修改、哪些文件是新添加的,以及哪些文件已经准备好被提交到仓库中。暂存区允许你将多个文件的更改分成一系列逻辑上相关的提交。