参考引用
- 阿秀的学习笔记
- 本博客对上述笔记进行较大程度的整合、精简与补充
21. define 宏定义和 const 的区别
- define 是在预编译阶段起作用,属于文本插入替换,而 const 是在编译、运行的时候起作用
- define 只做替换,不做类型检查和计算,容易出错,const 常量有数据类型,编译器可以对其进行类型安全检查
- define 只是将宏名称进行替换,在内存中会产生多份相同的备份,const 在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的表达式计算出结果放入常量表
22. C++ 中 const 和 static 的作用
-
static 不考虑类的情况
- 隐藏。所有不加 static 的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用
- 默认初始化为 0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区
- 静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用
-
static 考虑类的情况
- static 成员变量
- 所有对象共享同一份数据,在编译阶段分配内存,类内声明类外初始化
- 只与类关联,不与类的对象关联
- 定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为 static
- 可以被非 static 成员函数任意访问
- static 成员函数
- 所有对象共享同一个函数,静态成员函数只能访问静态成员变量
- 静态成员函数属于类,而非特定对象实例,所以无法使用 this 指针,无法访问类对象的非 static 成员变量和非 static 成员函数
- 可以被非 static 成员函数任意访问
- 不能被声明为 const 和 virtual
1、static 成员函数是类所有对象的共享成员,不属于任何类对象或类实例,所以即使给此函数加上 virutal 也是没有任何意义的,virtual 虚函数依靠 vptr 和 vtable 来处理,vptr 是一个指针,在类的构造函数中创建生成,并且只能用 this 指针来访问它,因为它是类的一个成员,而 static 成员函数不具有 this 指针,虚函数的调用关系:this -> vptr -> vtable ->virtual function
2、当声明一个非静态成员函数为 const 时,对 this 指针会有影响,而 static 成员函数没有 this 指针,所以使用 const 来修饰 static 成员函数没有任何意义
- static 成员变量
-
const 不考虑类的情况
- const 常量在定义时必须初始化,之后无法更改
- const 形参可以接收 const 和非 const 类型的实参
- const 修饰变量与 static 有一样的隐藏作用:只能在该文件中使用,其他文件不可使用
-
const 考虑类的情况
- const 成员变量
- 不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数
- 不同类对其 const 数据成员的值可以不同,所以不能在类中声明时初始化
- const 成员函数
- const 对象不可以调用非 const 成员函数,非 const 对象都可以调用
- 不可以改变非 mutable(用该关键字声明的变量可以在 const 成员函数中被修改)数据的值
- const 成员变量
23. 数组名和指针(这里为指向数组首元素的指针)区别
- 二者均可通过增减偏移量来访问数组中的元素
- 数组名不是真正意义上的指针,数组名没有自增、自减等操作
- 当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但 sizeof 运算符不能再得到原数组的大小了
数组和指针的区别
- 存储方式。数组在内存中连续存放,开辟一块连续的内存空间,数组是根据数组下标进行访问的;指针相当于一个变量,可以指向任意类型的数据,指针的类型说明了它所指向地址空间的内存,指针的存储空间不能确定
- 访问方式。数组的元素可以通过下标来访问,下标从 0 开始,最大下标为数组大小减 1;指针可以通过加减运算来访问它所指向的数组元素,但需要注意指针的类型,以及要访问的数组元素的类型
- 动态分配。指针可以通过malloc()和free()来分配空间和释放空间,这是动态分配空间;数组是隐式分配和删除,这是静态分配空间
- 安全性。指针使用不当会造成内存泄漏,数组使用不当会造成数组越界;指针名是变量,数组名是指针常量,所以指针 p 可以进行 p++,而数组名不可以用于 a++
24. override 和 final 关键字
-
override
- 当在父类中使用了虚函数,可能需要在某个子类中对这个虚函数进行重写
- override 指定子类的这个虚函数是重写父类的,如果函数名不小心打错,编译器不会编译通过
class A { virtual void foo(); } class B : public A { void foo(); // OK,对虚函数 foo() 进行重写 virtual void foo(); // OK,对虚函数 foo() 进行重写 void foo() override; // OK,对虚函数 foo() 进行重写 }
-
final
- 当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加 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 {};
25. 拷贝初始化和直接初始化
- 当用于类类型对象时
- 直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数
- 拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象
string str1("I am a string"); // 语句1 直接初始化 string str2(str1); // 语句2 直接初始化,str1是已经存在的对象,直接调用拷贝构造函数对str2进行初始化 string str3 = "I am a string"; // 语句3 拷贝初始化,先为字符串创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3 string str4 = str1; // 语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数
为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样拷贝初始化就完全等价于直接初始化了,但是需要辨别两种情况
- 拷贝构造函数为 private 时:语句 3 和语句 4 在编译时会报错
- 使用 explicit 修饰构造函数时:如果构造函数存在隐式转换,编译时会报错
26. extern 关键字的用法
- extern 用于变量
- 当需要在多个文件中共享同一个全局变量时,可以使用 extern 关键字
// 文件1: main.cpp extern int shared_var; // 声明一个外部整型变量 int main() { shared_var = 10; // 使用外部变量 return 0; } // 文件2: shared.cpp int shared_var = 0; // 定义一个全局整型变量
- extern 用于函数
- extern 关键字也可以用于在一个文件中引用另一个文件中定义的函数
// 文件1: main.cpp extern void print_message(); // 声明一个外部函数 int main() { print_message(); // 调用外部函数 return 0; } // 文件2: print.cpp #include <iostream> void print_message() { // 定义一个函数 std::cout << "Hello, World!" << std::endl; }
- extern “C” 的用法
- 如果想在 C++ 代码中调用 C 代码,或者在 C 代码中调用 C++ 代码,就需要用到 extern “C”
// 文件1: main.cpp (C++ 代码) extern "C" void print_message(); // 使用 extern "C" 声明一个外部函数 int main() { print_message(); // 调用外部函数 return 0; } // 文件2: print.c (C 代码) #include <stdio.h> void print_message() { // 定义一个函数 printf("Hello, World!\n"); }
27. 野指针和悬空指针
都是指向无效内存区域(“不安全不可控”)的指针,访问行为将会导致未定义行为
- 野指针,指的是没有被初始化过的指针
- 解决办法:1、定义指针变量及时初始化,或者先置空 NULL 可防止指针误用;2、在指针被释放之后,将其赋值为 NULL 或 nullptr;3、使用智能指针,智能指针可以自动管理动态内存,避免内存泄漏和野指针问题 ;4、避免指针操作过程中越界访问数组,可以使用STL容器代替数组,STL容器可以自动管理内存
int* p; // 未初始化 std::cout<< *p << std::endl; // 未初始化就被使用
- 悬空指针,指针最初指向的内存已经被释放的指针
- 解决办法:释放操作后立即置空
- C++ 引入了智能指针,目的就是避免悬空指针的产生
int* p = nullptr; int* p2 = new int; p = p2; delete p2;
28. 重载、重写/覆盖和隐藏的区别
-
重载(overload)
- 同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现:参数个数和类型均相同,仅仅依靠返回值不同来区分的函数的情况
- 重载是不同函数之间的水平关系
- 重载要求参数列表不同,返回值不要求
-
重写/覆盖(override)
- 重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且与基类的虚函数:有相同的参数个数、相同的参数类型、相同的返回值类型
- 重写是父类和子类之间的垂直关系
- 重写要求参数列表相同
-
隐藏(hide)
- 隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数
29. 构造函数和析构函数
-
对象的初始化和清理工作是编译器强制要做的事情,因此如果程序员不提供构造和析构,编译器会主动提供,但编译器提供的构造函数和析构函数是空实现,构造函数和析构函数作用如下:
- 构造函数:创建对象时为对象的成员属性赋值,构造函数由编译器自动调用(只调用一次),无须手动调用
- 析构函数:析构函数在对象销毁前由系统自动调用(只调用一次),执行一些清理工作
构造函数可以有参数,因此可以发生重载,而析构函数没有参数,因此不可以发生重载
-
构造函数的分类及调用
- 按参数分为:有参构造和无参构造(默认构造函数)
- 按类型分为:普通构造和拷贝构造
// 1、构造函数分类 class Person { public: // 1.1 无参(默认)构造函数 Person() { cout << "无参构造函数!" << endl; } // 1.2 有参构造函数 Person(int a) { age = a; cout << "有参构造函数!" << endl; } // 1.3 拷贝构造函数 Person(const Person& p) { age = p.age; cout << "拷贝构造函数!" << endl; } // 析构函数,在释放内存之前调用 ~Person() { cout << "析构函数!" << endl; } public: int age; }; // 2、构造函数的调用 // 2.1 调用无参构造函数 void test01() { Person p; // 调用无参构造函数 } // 2.2 调用有参的构造函数 void test02() { // 2.2.1 括号法,最常用 Person p1(10); // 注意 1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明 //Person p2(); // 2.2.2 显式法 Person p2 = Person(10); Person p3 = Person(p2); //Person(10)单独写就是匿名对象 当前行结束之后,马上析构 // 2.2.3 隐式转换法 Person p4 = 10; // Person p4 = Person(10); Person p5 = p4; // Person p5 = Person(p4); // 注意 2:不能利用拷贝构造函数初始化匿名对象,编译器认为是对象声明 // Person p5(p4); } int main() { test01(); //test02(); system("pause"); return 0; }
-
拷贝构造函数调用时机
// 1. 使用一个已经创建完毕的对象来初始化一个新对象 void test01() { Person man(100); // p 对象已经创建完毕 Person newman(man); // 调用拷贝构造函数 Person newman2 = man; // 拷贝构造 //Person newman3; //newman3 = man; // 不是调用拷贝构造函数,赋值操作 } // 2. 值传递的方式给函数参数传值 // 相当于 Person p1 = p; void doWork(Person p1) {} void test02() { Person p; //无参构造函数 doWork(p); } // 3. 以值方式返回局部对象 Person doWork2() { Person p1; cout << (int *)&p1 << endl; return p1; } void test03() { Person p = doWork2(); cout << (int *)&p << endl; } int main() { //test01(); //test02(); test03(); system("pause"); return 0; }
-
构造函数调用规则
- 默认情况下,C++ 编译器至少给一个空类添加 6 个默认成员函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数(对属性进行值拷贝)
- 赋值操作符重载
- 取地址操作符重载
- const 修饰的取地址操作符重载
- 如果用户定义有参构造函数,C++ 不再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++ 不再提供其他构造函数
- 默认情况下,C++ 编译器至少给一个空类添加 6 个默认成员函数
-
类成员初始化
- 类成员初始化方式:赋值初始化、成员列表初始化
- C++ 的赋值操作会产生临时对象,临时对象的出现会降低程序的效率,因此成员列表初始化方式更快,如果是在构造函数体内进行赋值的话,等于是一次默认构造加一次赋值,而初始化列表只做一次赋值操作
class Person { public: // 1、赋值初始化 //Person(int a, int b, int c) { // m_A = a; // m_B = b; // m_C = c; //} // 2、成员列表初始化 Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c) {} private: int m_A; int m_B; int m_C; };
- 必须使用成员列表初始化的四种情况
- 当初始化一个引用成员时
- 当初始化一个常量成员时
- 当调用一个基类的构造函数,而它拥有一组参数时
- 当调用一个成员类的构造函数,而它拥有一组参数时
-
(派生类)构造函数的执行顺序
- 1、虚基类的构造函数
- 2、基类的构造函数
- 3、成员类对象的构造函数
- 4、派生类自己的构造函数
析构函数顺序正好与上面顺序相反
-
构造函数、拷贝构造函数和赋值操作符的区别
- 构造函数
- 对象不存在,没用别的对象初始化,在创建一个新的对象时调用构造函数
- 拷贝构造函数
- 对象不存在,但是使用别的已经存在的对象来进行初始化
- 赋值运算符
- 对象存在,用别的对象给它赋值,属于重载 “=” 号运算符的范畴,“=” 号两侧的对象都是已存在的
拷贝构造函数是函数,赋值运算符是运算符重载,拷贝构造函数会生成新的类对象,赋值运算符不能
- 构造函数
30. 浅拷贝和深拷贝的区别
- 浅拷贝
- 浅拷贝只是简单的赋值拷贝操作,只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝指针的资源就会出现错误
- 深拷贝
- 深拷贝不仅拷贝值,还在堆区重新申请空间用来存放新的值,即使原先的对象被析构掉释放内存了,也不会影响到深拷贝得到的值
- 在实现拷贝赋值的时候,如果有指针变量的话是需要实现深拷贝的
#include <iostream> using namespace std; class Person { public: // 无参(默认)构造函数 Person() { cout << "non para construct!" << endl; } // 有参构造函数 Person(int age, int height) { cout << "have para construct!" << endl; m_age = age; m_height = new int(height); } // 拷贝构造函数 Person(const Person& p) { cout << "copy construct!" << endl; // 如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题 m_age = p.m_age; m_height = new int(*p.m_height); } // 析构函数 ~Person() { cout << "~!" << endl; if (m_height != NULL) { delete m_height; } } public: int m_age; int* m_height; }; int main() { Person p1(18, 180); Person p2(p1); cout << "p1 age: " << p1.m_age << " height: " << *p1.m_height << endl; cout << "p2 age: " << p2.m_age << " height: " << *p2.m_height << endl; system("pause"); return 0; }
have para construct! copy construct! p1 age: 18 height: 180 p2 age: 18 height: 180 ~! ~!
31. C++ 类的访问权限和继承权限
-
访问权限
-
继承权限:protected 继承和 private 继承
public继承
- 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,都保持原有的状态,而基类的私有成员任然是私有的,不能被这个派生类的子类所访问
友元(friend)
- 有些私有属性也想让类外一些特殊的函数或者类进行访问,就需要用到友元的技术
- 友元的目的就是让一个函数或者类访问另一个类中私有成员
32. mutable 和 explicit 关键字
- mutable
- 成员函数后加 const 称为常函数,常函数内不可以修改成员属性
- 但如果成员属性声明时加关键字 mutable,则在常函数中依然可以修改
- explicit
- 用来修饰类的构造函数,被修饰的构造函数的类不能发生隐式类型转换,只能以显式方式进行类型转换
- explicit 关键字只能用于类内部的构造函数声明上
C++ 隐式转换
- 所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为
- 通过隐式转换,可以直接将一个子类的对象使用父类的类型进行返回
- 隐式转换发生在从小 --> 大的转换中:char --> int、int --> long
- 在构造函数声明的时候加上explicit关键字,能够禁止隐式转换
33. C++ 异常处理方法
-
常见的异常
- 数组下标越界
- 除法计算时除数为 0
- 动态分配空间时空间不足
-
异常处理方式
- 1. try、throw 和 catch 关键字:先执行 try 语句块,如果执行过程中没有异常发生,则不会进入任何 catch 语句块,如果发生异常,则使用 throw 进行异常抛出,再由 catch 进行捕获
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; }
- 2. C++ 标准异常类 exception
34. 形参与实参的区别
- 函数定义里小括号内的参数称为形参,函数调用时传入的参数称为实参
- 形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只有在函数内部有效,函数调用结束返回主调函数后则不能再使用该形参变量
- 实参可以是常量、变量、表达式和函数等,无论实参是何种类型的量,在进行函数调用时都必须具有确定的值,以便把这些值传送给形参
- 实参和形参在数量上、类型上和顺序上应严格一致,否则会发生 “类型不匹配” 的错误
- 只能把实参的值传送给形参,而不能把形参的值反向传送给实参,因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化
35. 值传递、指针传递和引用传递
- 值传递
- 当将一个值传递给函数时,函数会创建该值的副本,并在函数内部使用这个副本,如果值传递的对象是类对象或是大的结构体对象,将耗费一定的时间和空间(传值)
- 对于简单的数据类型,如整数、字符等,值传递是最合适的选择,当函数不需要修改原始值,只是使用该值时,值传递也是一个不错的选择
- 指针传递
- 将参数的内存地址传递给函数(传地址值)
- 当函数需要修改原始值时,指针传递是一个不错的选择,对于需要传递大型对象的情况,指针传递可以提高性能
- 引用传递
- 将参数的引用传递给函数,函数可以通过引用直接访问和修改原始值,而无需创建副本(传地址)
- 当函数需要修改原始值时,引用传递是最常用的方式,对于大型对象的传递,引用传递可以提高性能
- 指针传递和引用传递比值传递效率高,推荐使用引用传递
- 不能返回局部变量的引用,因为函数返回后局部变量就会被销毁
// 1. 值传递 void mySwap01(int a, int b) { int temp = a; a = b; b = temp; } // 2. 指针传递 void mySwap02(int* a, int* b) { int temp = *a; *a = *b; *b = temp; } // 3. 引用传递 void mySwap03(int& a, int& b) { int temp = a; a = b; b = temp; } int main() { int a = 10; int b = 20; mySwap01(a, b); cout << "a:" << a << " b:" << b << endl; mySwap02(&a, &b); cout << "a:" << a << " b:" << b << endl; mySwap03(a, b); cout << "a:" << a << " b:" << b << endl; system("pause"); return 0; }
- 指针传递和引用传递的使用时机(见 4)
- 对于使用引用的值而不做修改的函数
- 如果数据对象很小,如内置数据类型或者小型结构,则按照值传递
- 如果数据对象是数组,则使用 const 指针(唯一的选择)
- 如果数据对象是较大的结构,则使用 const 指针或者引用,已提高程序的效率
- 如果数据对象是类对象,则使用 const 引用(传递类对象参数的标准方式是按照引用传递)
- 对于修改函数中数据的函数
- 如果数据是内置数据类型,则使用指针
- 如果数据对象是结构,则使用引用或者指针
- 如果数据是类对象,则使用引用
- 对于使用引用的值而不做修改的函数
36. C++ 中 string 与 C 语言中的 char* 区别
- string 继承自 basic_string,是对 char* 进行了封装,封装的 string 包含了 char* 数组、容量和长度等属性
- string 可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2*n),然后将原字符串拷贝过去,并加上新增的内容
37. C++ 的四种强制转换
- static_cast
- 没有运行时类型检查来保证转换的安全性
- 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用引用的转换
- 进行上行转换(派生类的指针或引用 --> 基类)是安全的
- 进行下行转换(基类的指针或引用 --> 派生类)时,由于没有动态类型检查,所以是不安全的
- 用于基本数据类型之间的转换:int --> char,int --> enum,这种转换的安全性也要程序员来保证
- 把空指针转换成目标类型的空指针
- 把任何类型的表达式转换成 void 类型
// 把 expression 转换为 type-id 类型 static_cast <type-id> (expression)
- dynamic_cast
- dynamic_cast 具有类型检查的功能,比 static_cast 更安全
- 基类向派生类转换(下行转换)比较安全
- 派生类向基类转换(上行转换)不太安全
- 主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换
- 在类层次间进行上行转换时,dynamic_cast 和 static_cast 的效果是一样的
// type-id 必须是类的指针、类的引用或者 void* dynamic_cast <type-id> (expression)
- dynamic_cast 具有类型检查的功能,比 static_cast 更安全
- const_cast
- 用来修改类型的 const 或 volatile 属性
- 常量指针被转化成非常量的指针,并且仍然指向原来的对象
- 常量引用被转换成非常量的引用,并且仍然指向原来的对象
- const_cast 一般用于修改底指针,如 const char *p 形式
const_cast<type_id> (expression)
- reinterpret_cast
- type-id 必须是一个指针、引用、算术类型、函数指针或成员指针,用于类型之间进行强制转换
reinterpret_cast<type-id> (expression)
38. 全局变量和局部变量
- 生命周期
- 全局变量随主程序创建和创建,随主程序销毁而销毁
- 局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在
- 使用方式
- 通过声明后全局变量在程序的各个部分都可以用到
- 局部变量分配在堆栈区,只能在局部使用
- 内存分配的位置
- 全局变量分配在全局数据段并且在程序开始运行的时候被加载
- 局部变量则分配在堆栈里面
非静态全局变量和静态全局变量
- 都是采取静态存储方式(全局变量前再冠以 static 就构成静态全局变量)
- 非静态的全局变量在各个源文件中都是有效的,而静态全局变量只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它,静态全局变量只初始化一次,防止在其他文件单元被引用
39. C++ 函数调用的压栈过程
- 1、从栈空间分配存储空间
- 2、从实参的存储空间复制值到形参栈空间
- 3、进行运算
#include <iostream> using namespace std; int f(int n) { cout << n << endl; return n; } void func(int param1, int param2) { int var1 = param1; int var2 = param2; // 如果将 printf 换为 cout 进行输出,输出结果则刚好相反 printf("var1 = %d, var2 = %d", f(var1), f(var2)); } int main(int argc, char* argv[]) { func(1, 2); return 0; }
2 1 var1 = 1, var2 = 2
- 当函数从入口函数 main 函数开始执行时,编译器会将操作系统的运行状态,main 函数的返回地址、main 的参数、mian 函数中的变量、进行依次压栈
- 当 main 函数开始调用 func() 函数时,编译器此时会将 main 函数的运行状态进行压栈,再将 func() 函数的返回地址、func() 函数的参数从右到左、func() 定义变量依次压栈
- 当 func() 调用 f() 的时候,编译器此时会将 func() 函数的运行状态进行压栈,再将 f() 的返回地址、f() 函数的参数从右到左、f() 定义变量依次压栈
40. C++ 的内存分区
-
栈区(向下增长)
- 存放函数的参数值、局部变量等
- 由编译器自动分配释放,栈内存分配运算内置于处理器的指令集中,效率很高,但分配的内存容量有限
-
堆区(向上增长)
- 由程序员分配和释放,若程序员不释放,程序结束时由操作系统自动回收
- 由 new 关键字在堆区开辟内存,一个 new 就要对应一个 delete
-
自由存储区
- 堆是操作系统维护的一块内存,自由存储区是 C++ 中通过 new 和 delete 动态分配和释放对象的抽象概念
- 自由存储区和堆比较像,但不等价
-
全局/静态存储区
- 存放全局变量和静态变量
- C 语言中全局变量和静态变量又分为初始化的和未初始化的,C++ 则没有区分,它们共同占用同一块内存区,在该区定义的变量若没有初始化则会被自动初始化,例如 int 型变量自动初始为 0
-
常量存储区
- 存放常量,不允许修改
-
代码区
- 存放函数体的二进制代码,由操作系统进行管理的
- 存放函数体的二进制代码,由操作系统进行管理的
-
程序运行前:在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域
- 代码区:存放 CPU 执行的机器指令
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
- 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
- 全局区:
- 全局变量和静态变量存放在此
- 全局区还包含了常量区,字符串常量和其他常量也存放在此
该区域的数据在程序结束后由操作系统释放
- 代码区:存放 CPU 执行的机器指令
-
程序运行后
- 栈区(见上)
- 堆区(见上)
C++ 内存的三种分配方式
- 从静态存储区分配:此时的内存在程序编译的时候已经分配好,并且在程序的整个运行期间都存在。全局变量,static 变量等在此存储
- 在栈区分配:相关代码执行时创建,执行结束时被自动释放。局部变量在此存储。栈内存分配运算内置于处理器的指令集中,效率高,但容量有限
- 在堆区分配:动态分配内存。用 new/malloc 开辟,delete/free 释放。生存期由用户指定,灵活,但有内存泄露等问题