5 数据抽象和类II
内存布局与对象
正文
- 函数实现,库实现,字符串等资源,不可改
char *s1 - "Literal"; //文字在代码区,仅分配了字符指针
// s1[0] = 'I' //不能修改
char s2[] = "Initial Literal"; //分配数组空间
// s2可以被修改
Point *po = &((Point){2, 3}); //分配结构空间且指针
// 取临时对象的地址给po,语句执行完毕之后临时对象就没了
静态变量(对象)
- main执行前初始化数据
- 用零初始化
栈(Stack)
- 函数参数,自动变量(对象)
堆(Heap)
- 动态变量(对象),由stdlib.h管理

总结
从低地址到高地址:
- 代码区,存放函数代码,是只读的
- 全局/静态区,存放数据(字符串字面量、已初始化/未初始化的全局变量和静态变量)
- 堆区,程序员手动分配
-
栈区,编译器自动管理的函数被调用时的局部变量,函数返回时这些变量自动销毁
-
上述代码里的
s1指向的字符串字面量在静态区,不可修改,s2整个数组在栈上,它的内容是"Initial Literal"的一个副本。
C语言动态对象管理
- 必须
#include<stdlib.h> - 申请空间
void * malloc(size_t) - 释放空间
void free(void *)
Point *p1 = malloc(sizeof(Point) * 10);
Point (*p2)[2][3] = malloc(sizeof(Point) * 6);
//指向2*3数组的指针
int n = 2, m = 3;
Point **p3 = malloc(sizeof(Point*) * n);
//指针的指针
for(size_t i = 0; i < n; i++){
p3[i] = malloc(sizeof(Point) * m);
}
free(p1);
free(p2);
for(size_t i = 0; i < n; i++){
free(p3[i]);
}
free(p3);//先释放行数组,再释放指针的指针
new运算符
- 指针 = new 类型名
- 指针 = new 类型名(初始化参数)
-
指针 = new 类型名[数组长度]
-
类型可以为基本类型,也可以为类(class)类型,如果为类,则初始化参数相当于将实际参数传递给该类的构造函数。
- new运算符返回一个该类型的指针,指向分配到的内存空间
- 若内存分配失败,抛出异常,不是NULL
delete运算符
- delete 变量名
-
delete[] 数组名
-
如果分配了数组但是用
delete p的方式释放,没有用[],会导致分配的数组没有完全释放 - 释放之后指针仍然指向原来的地址,但指针已无效,重复释放将出错
- delete对象指针会调用该对象的析构函数
内存泄漏
指new的空间失去了指针或引用永远无法释放,导致资源耗尽。
int *p = new int(7);
p = nullptr; // 内存泄漏
void f(){
int *p = new int(7);
}
//离开作用域,指针没有了,内存泄漏
显式定义默认函数
当我们声明有参构造函数时,编译器不会创建默认构造函数。=default是令编译器为某个类生成特殊成员函数或比较运算符的显式指令(C++11)。特殊成员函数包括:
- 默认构造函数
T() - 析构函数
~T() - 拷贝构造函数
T(const T&) - 赋值运算
operator=(const T&)
=default必须在1函数声明后,让编译创建该构造函数。
class A
{
public:
A(int x){
cout << "This is a parameterized constructor";
}
A() = default;
};
弃置函数
在 C++ 中,=delete 是一个用于显式禁止某个函数被调用的语法特性。你可以把它理解为给函数贴上一个“禁止通行”的标签。一旦某个函数被标记为 =delete,任何尝试调用它的代码都会在编译期直接报错,从而有效防止误用。
请注意,删除的函数是隐式内联的,这一点非常重要。删除的函数定义必须是函数的首次声明。
成员初始化器
在 C++ 中,“成员初始化器”通常指的是构造函数初始化列表。是一种在构造函数体执行之前,直接初始化类成员变量的语法机制。
语法结构
成员初始化器位于构造函数的参数列表之后,构造函数体之前,以冒号 : 开头,后面跟着一系列以逗号分隔的成员初始化项。
class MyClass {
private:
int x;
int y;
std::string name;
public:
// 构造函数
// x(a) 和 name(n) 就是成员初始化器
MyClass(int a, const std::string& n) : x(a), name(n) {
// 构造函数体
y = 0; // 这是在函数体内赋值,不是初始化
}
};
为什么要使用成员初始化器
性能优化:
如果你在构造函数体内使用赋值操作(例如 x = a;),对于非内置类型(如 std::string 或自定义类),编译器会执行以下步骤:
1. 默认构造:先调用成员的默认构造函数(例如创建一个空字符串)。
2. 赋值:再调用赋值运算符,将新值赋给它。
这相当于做了两次工作。而使用成员初始化器,编译器会直接调用带参数的构造函数进行初始化,只执行一次,效率更高。
强制需求:
在以下三种情况下,你必须使用成员初始化器,否则代码无法通过编译:
const成员变量:常量一旦创建就不能被修改,因此必须在创建时(即初始化阶段)赋值,不能在函数体内赋值。- 引用成员变量:引用必须在定义时绑定到一个有效的对象,不能先定义空引用再赋值。
- 没有默认构造函数的类成员:如果成员是一个类对象,且该类没有默认构造函数(即必须传参才能构造),则必须在初始化列表中显式传递参数。
初始化顺序
这是一个非常容易被忽视但至关重要的规则:
成员的初始化顺序,完全取决于它们在类中声明的顺序,而与初始化列表中书写的顺序无关。
class Example {
private:
int a;
int b;
public:
// 看起来是先初始化 b,再初始化 a
// 但实际上:a 先被初始化(因为它在类中先声明),b 后被初始化
Example(int val) : b(val), a(b) {
// 此时 a 的值是未定义的!因为 b 还没初始化,a 就尝试用 b 的值
}
};
对象成员初始化器
对象成员初始化器就是在创建对象时,用 {} 直接给成员变量赋值的语法糖。
语法糖(Syntactic Sugar),又称糖衣语法,是由英国计算机科学家彼得·约翰·兰达提出的术语,指编程语言中添加的简化语法结构。这类语法不改变语言功能,但能提升代码可读性并减少错误率,例如C语言中a[i]等价于指针表达式*(a+i)。
举例
假设有个类:
struct Student {
int id;
string name;
};
用对象成员初始化器创建对象:
Student s = {1001, "张三"};
Student s{1001, "张三"}; // C++11 统一初始化
类数组
struct Student {
int id;
string name;
};
// 类数组,直接用 {} 初始化每个对象
Student arr[] = {
{101, "小明"},
{102, "小红"},
{103, "小李"}
};
Student arr[] {
{101, "小明"},
{102, "小红"},
{103, "小李"}
};
只要你的类/结构体满足:
- 没有自定义构造函数
- 没有
private成员(或成员可访问) - 是简单的聚合类(aggregate)
就可以用 对象成员初始化器 初始化数组。
与new结合
申请数组:new 类型[]{初始化列表}
int *p;
int Length, i;
cin >> Length;
p = new int[Length]{1,3,5};
用new给类分配内存时:
- 如果后面没有初始化列表,会调用无参构造(若为默认生成的无参构造,会初始化为不确定值)
- 常数初始化列表(
int[]、double[]),用初始化列表赋值,元素不够时,后边补0。例如上述代码中Length大于3时。 - 如果是类类型数组,不够 → 剩余元素用 默认构造函数(无参构造)初始化。
- 如果给数组用了对象成员初始化器,数组所需的参数长度小于初始化序列长度,则抛出异常
特点
- 按成员声明顺序赋值
- 适合简单结构体、类
nullptr
在现代 C++ (C++11 及以后) 中,nullptr 和 NULL 的核心区别在于类型安全。nullptr 是专门用于表示空指针的关键字,而 NULL 是一个代表整数值 0 的宏,容易在特定场景下引发歧义。