CS100 学习笔记 - C++语言基础部分

CS100 学习笔记 - C++语言基础部分

记录一些规范和自己不知道的特性。

Lesson 11

什么是C++?

Effective C++ Item 1 (by Scott Meyers): View C++ as a federation of languages.

The easiest way is to view C++ not as a single language but as a federation of related languages ... Fortunately, there are only four:

  • C.
  • Object-Oriented C++.
  • Template C++.
  • The STL.

C++ 中的 C

C++ 标准库包含了 C 标准库的设施,但并不完全一样

  • 因为一些历史问题(向后兼容),C 有很多不合理之处,例如 strchr 接受 const char * 却返回 char *,某些本应该是函数的东西被实现为宏。
  • C 缺乏 C++ 的 function overloading 等机制,因此某些设计显得繁琐。
  • C++ 的编译期计算能力远远强过 C,例如 <cmath> 里的数学函数自 C++23 起可以在编译时计算。

C++ 的标准库文件 没有后缀名: <iostream> instead of <iostream.h>, <string> instead of <string.h>.
C 的标准库文件 <xxx.h> 在 C++ 中的版本是 <cxxx>,并且所有名字也被引入了 namespace std

更合理的设计:

  • booltruefalse 是内置的,不需要额外头文件
  • 逻辑运算符和关系运算符的返回值是 bool 而非 int
  • "hello" 的类型是 const char [6] 而非 char [6]
  • 字符字面值 'a' 的类型是 char 而非 int
  • 所有有潜在风险的类型转换都不允许隐式发生,不是 warning,而是 error。
  • const int maxn = 100; 声明的 maxn 是编译期常量,可以作为数组大小。
  • int fun() 不接受参数,而非接受任意参数。

IO stream

std::cinstd::cout 是定义在 <iostream> 中的两个对象,分别表示标准输入流和标准输出流。

cppreference IO库 | cppreference IO操纵符

std::cinstd::cout 是“对象”而非“函数”。要和学生强调术语的规范性。再比如,“调用”这个词一般只能用于函数, #include <iostream> 并不是在“调用标准库”,使用 ab 的值也并不是在“调用 ab ”。要正确地使用术语,不要自己发明术语,不要羞于使用术语而使用一些表意不明的口头语,不要乱用术语。

namespace std

C++ 有一套非常庞大的标准库,为了避免名字冲突,所有的名字(函数、类、类型别名、模板、全局对象等)都在一个名为 std命名空间下。

  • 你可以用 using std::cin;cin 引入当前作用域,那么在当前作用域内就可以省略 std::cinstd::
  • 你可以用 using namespace std;std 中的所有名字都引入当前作用域,但这将使得命名空间形同虚设,并且重新引入了名字冲突的风险。(我个人极不推荐,并且我自己从来不写)

CS100 课程中不允许在头文件的 全局作用域 中使用以上任何一种 using

std::string

string#include <string>

定义与初始化

std::string str = "Hello world";                        // 复制初始化,不是赋值
// equivalent: std::string str("Hello world");          // 直接初始化
// equivalent: std::string str{"Hello world"}; (modern) // 直接初始化,不是列表初始化
std::string s1(7, 'a'); // aaaaaaa                      // 直接初始化
std::string s2 = s1; // s2 is a copy of s1              // 复制初始化,不算赋值
std::string s; // "" (empty string)                     // 默认初始化
  • std::string 的内存:自动管理,自动分配,需要时自动增长,自动释放
  • 使用 std::string 时,关注字符串的内容本身,而非它的实现细节
    • 不必再考虑它的内存是怎么管理的,不必考虑末尾是不是有 '\0'

运算 & 赋值

可以用 +=+ 拼接字符串,返回 string 类型。
s1 = s1 + s2 会先为 s1 + s2 构造一个临时对象,必然要拷贝一遍 s1 的内容。
s1 += s2 是直接在 s1 后面连接 s2

std::string hello{"hello"};
std::string s0 = hello + "world";
std::string s1 = "world" + hello;
s0 += "C++";
std::string s3 = hello + "world" + "C++"; //OK,因为 + 是左结合的
                                          // 相当于 (hello + "world") + "C++"

比较:<, <=, >, >=, ==, !=。赋值:=

std::getline(std::cin, s):从当前位置开始读一行,换行符会读掉,但不会存进来。 假如前一次输入恰好停在换行符处, getline 就会读进一个空串。

遍历字符串:基于范围的 for 语句

例:输出所有大写字母(std::isupper<cctype> 里)

for (char c : s)
  if (std::isupper(c))
    std::cout << c;
std::cout << std::endl;

等价的方法:使用下标,但不够 modern,比较啰嗦。

for (std::size_t i = 0; i != s.size(); ++i)
  if (std::isupper(s[i]))
    std::cout << s[i];
std::cout << std::endl;

[Best practice] Use range-based for loops. They are modern, clear, simple, generic, and hence more recommended.

  • 你的意图是“遍历该字符串”,而非“创建一个整数并使它从 0 变化到 s.size() ”。

转换

对任意数值类型 x, std::to_string(x) 返回它的字符串形式. See this list.
std::stoi(s), std::stol(s), ...: Extracts the arithmetic value represented by s

Lesson 12

左值和右值

一个表达式在被使用时,有时我们使用的是它代表的对象,有时我们仅仅是使用了那个对象的

  • str[i] = ch 中,我们使用的是表达式 str[i] 所代表的对象
  • ch = str[i] 中,我们使用的是表达式 str[i] 所代表的对象的

一个表达式本身带有值类别 (value category) 的属性:它要么是左值,要么是右值

  • 左值:它代表了一个实际的对象
  • 右值:它仅仅代表一个值

在 C 中,左值可以放在赋值语句的左侧,右值不能。但在 C++ 中,二者的区别远没有这么简单。

  • 返回左值的表达式:*p, a[i]
  • 特别地:在 C++ 中,前置递增/递减运算符返回左值++i = 42 是合法的。
  • 赋值表达式返回左值a = b 的返回值是 a 这个对象(的引用)。
    • 赋值运算符右结合,表达式 a = b = c 等价于 a = (b = c)

右值仅仅代表一个值,不代表一个实际的对象。常见的右值有表达式执行产生的临时对象字面值

  • 函数调用 fun() 生成的临时对象是右值
  std::string fun(); // a function that returns a std::string object
  std::string a = fun();
  • 特别的例外:字符串字面值 "hello" 是左值,它是长期存在于内存中的对象。
    • 相比之下,整数字面值 42 仅仅产生一个临时对象,是右值。
  • 通过类型转换生成的临时对象
std::string &r1 = std::string("hello"); // Error
std::string &r2 = "hello"; // Error. This is equivalent to ↑

Functional-style cast expression: Type(args...) ,会生成一个 Type 类型的临时对象。

  • 对于类类型,这会调用一个适当的构造函数(或者类型转换运算符)
    • 例如 std::string(10, 'c'), std::string("hello")
  • 对于内置类型,就是一个普通的拷贝或者类型转换
    • int(x) 会生成一个 int 类型的临时对象,其值由 x 初始化。

真正的“值类别”

(语言律师需要掌握)

C++ 中的表达式依值类别被划分为如下三种:

英文 中文 has identity? can be moved from?
lvalue 左值 yes no
xvalue (expired value) 亡值 yes yes
prvalue (pure rvalue) 纯右值 no yes

lvalue + xvalue = glvalue(广义左值),xvalue + prvalue = rvalue(右值)

  • 所以实际上“左值是实际的对象”是不严谨的,右值也可能是实际的对象(xvalue)。之后讲移动的时候我们会见到一个典型的 xvalue 。

引用

引用类型 ReferredType & ,相当于在初始化时将该变量与另一个变量绑定,作为被绑定对象的别名。
引用必须初始化(即在定义时就指明它绑定到谁),并且这个绑定关系不可修改。

References must be bound to existing objects ("lvalues")!
引用绑定到的类型必须是左值(对象类型:普通的变量、数组、指针)。引用不允许绑定临时对象与字面值(非左值):

int &r1 = 42;    // Error: binding a reference to a literal
int &r2 = 2 + 3; // Error: binding a reference to a temporary object
int a = 10, b = 15;
int &r3 = a + b; // Error: binding a reference to a temporary object

(C++11 引入了所谓的“右值引用”。一般来说,“引用”指的是“左值引用”。)

引用是一个左值。它并不是一个对象,所以你不能创建引用的引用(同样地,指针指向的类型、数组的元素类型也不能是引用)

int ival = 42;
int &ri = ival; // binding `ri` to `ival`.
//int & &rr = ri; // Error! No such thing!
int &ri2 = ri; // Same as `int &ri2 = ival;`.
//int & *pr = &r; // No such thing!
int *pi = &ri; // Same as `int *pi = &ival;`.
int (&ar)[10] = a; //绑定到数组的引用

类似指针有:

int& x = ival, y = ival, z = ival;
// Only `x` is a reference. `y` and `z` are of type `int`.

常量引用 reference-to-const

类似于“指向常量的指针”(即带有“底层 const”的指针),我们也有“绑定到常量的引用”。
一个 reference-to-const 自认为自己绑定到 const 对象,所以不允许通过它修改它所绑定的对象的值,也不能让一个不带 const 的引用绑定到 const 对象。(不允许“去除底层 const”)

指针既可以带顶层 const(本身是常量),也可以带底层 const(指向的东西是常量),但引用不谈“顶层 const”。

  • 即,只有“绑定到常量的引用”。引用本身不是对象,不谈是否带 const
  • 从另一个角度讲,引用本身一定带有“顶层 const”,因为绑定关系不能修改。
  • 在不引起歧义的情况下,通常用常量引用这个词来代表“绑定到常量的引用”。

特殊规则:常量引用可以绑定到右值:

  • 当一个常量引用被绑定到右值时,实际上就是让它绑定到了一个临时对象。
    • 这是合理的,反正你也不能通过常量引用修改那个对象的值

Example: Pass by reference-to-const

int count_lowercase(std::string &str)

函数在传参时会发生一次赋值产生的拷贝,这是没有必要的(因为不涉及对类的临时修改),所以我们在这里加一个引用规避拷贝。
但是这会产生一个问题,我们不能传入一个表达式(右值)或字符串常量,如 int result = count_lowercase(s1 + s2); 会报错。
(虽然字符串常量是左值,但当我们传递 "Hello"std::string 参数时,实际上发生了一个由 const char [6]std::string隐式转换,这个隐式转换产生右值,无法被 std::string& 绑定)

但由于常量引用可以绑任何的值,且同时我们不需要对参数做任何修改,于是参数可以这样定义:

int count_lowercase(const std::string &str)

[Best practice] Pass by reference-to-const if copy is not necessary and the parameter should not be modified.
将参数声明为常量引用,既可以避免拷贝,又可以允许传递右值,也可以传递常量对象,也可以防止你不小心修改了它。(如果仅仅是 int 或者指针这样的内置类型,可以不需要常量引用)

Example: Use references in range-for

for (char c : str)
  // ...
//等价于:
for (std::size_t i = 0; i != str.size(); ++i) {
  char c = str[i];
  // ...
}

可见 c 只是 str[i] 的一个拷贝,修改 c 无法修改 str[i] 。所以:

//change all lowercase letters to their uppercase forms
for (char &c : str)
  c = std::toupper(c);
//等价于
for (std::size_t i = 0; i != str.size(); ++i) {
  char &c = str[i];
  c = std::toupper(c); // Same as `str[i] = std::toupper(str[i]);`.
}

引用与指针对比

A reference

  • is not itself an object. It is an alias of the object that it is bound to.
  • cannot be rebound to another object after initialization.
  • has no "default" or "zero" value. It must be bound to an object.

A pointer

  • is an object that stores the address of the object it points to.
  • can switch to point to another object at any time.
  • can be set to a null pointer value nullptr. (C++ 里不要用 NULL

std::vector

std::vector 是一个类模板,只有给出了模板参数之后才成为一个真正的类型。编译器从类模板创建类的过程称为实例化

std::vector v;               // Error: missing template argument.
std::vector<int> vi;         // An empty vector of `int`s.
std::vector<std::string> vs; // An empty vector of strings.
std::vector<double> vd;      // An empty vector of `double`s.
std::vector<std::vector<int>> vvi; // An empty vector of vector of `int`s.
                                   // "2-d" vector.

初始化

std::vector<int> v{2, 3, 5, 7};     // A vector of `int`s,
                                    // whose elements are {2, 3, 5, 7}.
std::vector<int> v2 = {2, 3, 5, 7}; // Equivalent to ↑

std::vector<std::string> vs{"hello", "world"}; // A vector of strings,
                                    // whose elements are {"hello", "world"}.
std::vector<std::string> vs2 = {"hello", "world"}; // Equivalent to ↑

std::vector<int> v3(10);     // A vector of ten `int`s, all initialized to 0.
std::vector<int> v4(10, 42); // A vector of ten `int`s, all initialized to 42.

vector<T> v(n) 这种构造方式会将 n 个元素都值初始化 (value-initialization)(类似于 C 中的“空初始化”),而不是得到一串 indeterminant value 。(对于类类型来说,“值初始化”几乎就是调用默认构造函数进行初始化。)

创建其他 std::vector 的拷贝:

std::vector<int> v{2, 3, 5, 7};
std::vector<int> v2 = v; // `v2`` is a copy of `v`
std::vector<int> v3(v);  // Equivalent
std::vector<int> v4{v};  // Equivalent

C++17 CTAD

Class Template Argument Deduction:只要你给出了足够的信息,编译器可以自动推导元素的类型。

std::vector v{2, 3, 5, 7};  // vector<int>
std::vector v2{3.14, 6.28}; // vector<double>
std::vector v3(10, 42);     // vector<int>
std::vector v4(10);         // Error: cannot deduce template argument type
  • 怎样算是给出了“足够的信息”?你品。(具体规则细节略去)

成员函数:

v.size() and v.empty() : std::vector 的大小与是否空
v.clear() : 清空 std::vector (不要写愚蠢的 while (!v.empty()) v.pop_back();
v.push_back(x) :将元素 x 添加到 v 的末尾
v.pop_back() : 删除 std::vector 最后一个元素
v.back()v.front() : 分别获得最后一个元素、第一个元素的引用
\(\qquad\cdot\) v.back(), v.front(), v.pop_back()v 为空的情况下是 undefined behavior 。

遍历

基于范围的 for 语句:
std::vector<std::string> vs = some_strings();
for (const std::string &s : vs) // use reference-to-const to avoid copying
  std::cout << s << std::endl;
下标访问:

可以使用 v[i] 来获得第 i 个元素(i 的有效范围是 \([0,N)\),其中 N = v.size()

  • 越界访问是未定义行为,并且通常是严重的运行时错误
  • std::vector 的下标运算符 v[i] 并不检查越界,目的是为了保证效率。
    • 事实上标准库容器的大多数操作(比如刚才的 front, back, pop_back都没有边界检查,为了效率。
  • 一种检查越界的下标是 v.at(i),它会在越界时抛出 std::out_of_range 异常。
    • 不妨自己试一试。

STL 的风格

基本操作和低级操作自动执行:

  • 默认初始化,而非不确定的值。
  • 拷贝是自动完成的(Member-wise copy)。
  • 内存管理是自动完成的。

C++ 标准库的各种设施是也是讲究统一性的。(.at(), .front(), .back(), .push_back(x), .pop_back(), .clear() 等函数)(完整列表)

std::vector 的增长策略

假设现在有一片动态分配的内存,长度为 i
当第 i+1 个元素到来时,分配一片长度为 2*i 的内存,将原有的 i 个元素拷贝过来,将新的元素放在后面,释放原来的那片内存
而当第 i+2, i+3, ..., 2*i 个元素到来时,我们不需要分配新的内存,也不需要拷贝任何对象!

假设 \(n=2^m\),那么总的拷贝次数就是 \(\sum_{i=0}^{m-1}2^i=O(n)\)平均(“均摊”)一次 push_back 的耗时是 \(O(1)\)(常数),可以接受。

可见,改变 vector 的大小可能会导致它所保存的元素“搬家”,这会使得所有指针、引用、迭代器失效。因此不要在用基于范围的 for 语句遍历容器的同时改变容器的大小!

Lesson 13

更强的类型检查

缩小的类型转换是不保值的,即会损失数据(如 long longintintchar

Stroustrup 本打算ban掉所有的缩小类型的隐式转换。但最后,在C++中并没有完全禁止缩小的类型转换。在现代C++中,只有在特殊的上下文中才允许使用它们。

有些类型转换是有害的,这些隐式转换被 C++ ban 了

  • For T \(\neq\) U, T * and U * are different types. Treating a T * as U * leads to undefined behavior in most cases, but the C compiler gives only a warning!
  • void * is a hole in the type system. You can cast anything to and from it without even a warning.
char *pc = pi; // Warning in C, Error in C++
void *pv = pi; char *pc2 = pv; // Even no warning in C! Error in C++.
int y = pc;    // Warning in C, Error in C++
  • 强拆 const
const int x = 42, *pci = &x;
int *pi = pci; // Warning in C, Error in C++
++*pi;         // undefined behavior

显式转换

C++ 类型转换

C++ 有四种命名类型转换(named cast operators)

  • static_cast<Type>(expr)
  • const_cast<Type>(expr)
  • reinterpret_cast<Type>(expr)
  • dynamic_cast<Type>(expr)

比 C 的 (Type)expr 更文明。

const_cast<Type>(expr) 可以在 const 声明情况不同的类型之间转换。比如拆掉底层 const

int ival = 42;
const int &cref = ival;
int &ref = cref; // Error: casting away low-level constness
int &ref2 = const_cast<int &>(cref); // OK
int *ptr = const_cast<int *>(&cref); // OK

当然以此强行拆掉 const 修改 const 变量依然是未定义行为

reinterpret_cast<Type>(expr) 可以把一种类型的指针or引用换成另一种类型的指针or引用。

int ival = 42;
char *pc = reinterpret_cast<char *>(&ival);

可能导致运行时错误(因为它实际指向的位置的类型还是原来的类型),非必要不要用。

static_cast<Type>(expr) 一般情况下是普通的,常常看起来无害的类型转换()

double average = static_cast<double>(sum) / n;
int pos = static_cast<int>(std::sqrt(n));
// 特殊用法:
static_cast<std::string &&>(str) // converts to a xvalue
static_cast<Derived *>(base_ptr) // downcast without runtime checking

[Best practice] Minimize casting. (Effective C++ Item 27)
Type systems work as a guard against possible errors: Type mismatch often indicates a logical error.

[Best practice] When casting is necessary, prefer C++-style casts to old C-style casts.

  • With old C-style casts, you can't even tell whether it is dangerous or not!

类型推断

std::vector v(10, 42); // std::vector<int> v(10, 42);
std::cout << x << d << s; // cout 会推断出这些变量的类型

auto

占位类型说明符,可通过初始值推断类型(所以必须要有初始值)

auto x = 42;        // `int`, because 42 is an `int`.
auto &r = x;        // `int &`, because `x` is an `int`.
const auto &rc = r; // `const int &`.
auto *p = &rc;      // `const int *`, because `&rc` is `const int *`.

auto str = "hello"; // `const char *` 
// the type of `"hello"` is **`const char [6]`**

C++14起,auto 可以推断函数的返回值。
C++20起,auto 可以推断函数的参数。(相当于函数模版了)

auto 化简声明:

auto it = vs.begin(); //std::vector<std::string>::const_iterator

auto lets us enjoy the benefits of the static type system.
auto 声明lambda表达式:

auto lam = [](int x, int y) { return x + y; } // A lambda expression.

lambda表达式有自己的类型,但是只有编译器知道它是什么。

decltype

decltype(expr) 推断表达式 expr 的类型而不求值。推断出来的类型可以直接用。

auto fun(int a, int b) { // The return type is deduced to be `int`.
  std::cout << "fun() is called.\n"
  return a + b;
}
int x = 10, y = 15;
decltype(fun(x, y)) z; // Same as `int z;`.
                       // Unlike `auto`, no initializer is required here.
                       // The type is deduced from the return type of `fun`.

函数参数默认值

函数参数可以设置默认值,但必须在后几个参数,而且之后传参必定会按顺序逐一传入。

std::string get_screen(std::size_t height = 24, std::size_t width = 80,
                       char background = ' ');
auto default_screen = get_screen();             // 24x80, filled with ' '
auto larger_screen  = get_screen(66, 256);      // 66x256, filled with ' '
auto scr = get_screen('#'); // Passing the ASCII value of '#' to `height`.
std::string get_screen(std::size_t height = 24, std::size_t width,
                       char background); // Error.

函数重载

函数可以同名,但是参数必须不同。也即他们需要有不同的调用方式

int fun(int);
double fun(int);  // Error: functions that differ only in
                  // their return type cannot be overloaded.

void move_cursor(Coord to);
void move_cursor(int r, int c); // OK, differ in the number of arguments
  • Example : The following are the same for an array argument:

    void fun(int *a);
    void fun(int (&a)[10]);
    int ival = 42; fun(&ival); // OK, calls fun(int *)
    int arr[10];   fun(arr);   // Error: ambiguous call (重载有歧义)
    
    • For fun(int (&)[10]), this is an exact match.
    • For fun(int *), this involves an array-to-pointer implicit conversion. We will see that this is also considered an exact match.

对于以下函数:

void fun(int);
void fun(double);
void fun(int *);
void fun(const int *);

调用结果:

fun(42);   // fun(int)
fun(3.14); // fun(double)
const int arr[10];
fun(arr);  // fun(const int *)
fun(&ival) // fun(int *)
fun('a')   // fun(int)
fun(3.14f) // fun(double)
fun(NULL)  //We will see this later.

一般来说,很少有人真的会定义一组接受 char, int, long, double 的重载函数(除非你是在编写输入输出之类的函数),绝大多数时候我们都是拿不同的类类型以及不同的参数个数进行重载。

重载决议 -隐式转换序列的分级

常见的:

  1. 准确匹配,比如:
    • 相同类型
    • 到指针 (或函数) 类型的退化 (值变换)
    • 顶层 const 转换
  2. 底层 const 转换
  3. 整型提升或浮点提升
  4. 数值转换
  5. 类相关转换 (in later lectures).

尽管C++对行为有所定义,但写程序时需要规避令人迷惑的重载调用。同时不要滥用调用,尽可能明确函数的功能。

空指针

C语言中,NULL 是宏,可能是 (void *)0 也可能是整数 0 ,会对类型推断产生影响。

所以自 C++11 起引入了新的空指针 nullptr,类型为 std::nullptr_t (defined in <cstddef>),可以使类型推断与重载准确。

[Best practice] Use nullptr as the null pointer constant in C++.

auto 与基于范围的 for

//int str_to_int(const std::string &str) 下:
for (auto c : str) // char
for (const auto &s : strs) // const std::string &s

数组:

int arr[100] = {}; // OK in C++ and C23.
// The following loop will read 100 integers.
for (auto &x : arr) // int &
  std::cin >> x;

数组作为参数:

void print(int *arr) { // 等价于 void print(int arr[])
  for (auto x : arr) // Error: `arr` is a pointer, not an array.
    std::cout << x << ' ';
  std::cout << '\n';
}
void print(const int (&arr)[100]) {
  for (auto x : arr) // OK. `arr` is an array.
    std::cout << x << ' ';
  std::cout << '\n';
}

Note that only arrays of 100 ints can fit here.

int a[100] = {}; print(a); // OK.
int b[101] = {}; print(b); // Error.
double c[100] = {}; print(c); // Error.

传入任意长度的数组需要使用函数模版:

template <typename Type, std::size_t N>
void print(const Type (&arr)[N]) {
  for (const auto &x : arr)
    std::cout << x << ' ';
  std::cout << '\n';
}

Recitations 7

newdelete(初步)

new 表达式

动态分配内存,并构造对象

int *pi1 = new int;     // 动态创建一个默认初始化的 int
int *pi2 = new int();   // 动态创建一个值初始化的 int
int *pi3 = new int{};   // 同上,但是更 modern
int *pi4 = new int(42); // 动态创建一个 int,并初始化为 42
int *pi5 = new int{42}; // 同上,但是更 modern

对于内置类型:

  • 默认初始化 (default-initialization):就是未初始化,具有未定义的值
  • 值初始化 (value-initialization):类似于 C 中的“空初始化”,是各种零。

new[] 表达式

动态分配“数组”,并构造对象

int *pai1 = new int[n];          // 动态创建了 n 个 int,默认初始化
int *pai2 = new int[n]();        // 动态创建了 n 个 int,值初始化
int *pai3 = new int[n]{};        // 动态创建了 n 个 int,值初始化
int *pai4 = new int[n]{2, 3, 5}; // 动态创建了 n 个 int,前三个元素初始化为 2,3,5
                                 // 其余元素都被值初始化(为零)
                                 // 如果 n<3,抛出 std::bad_array_new_length 异常

deletedelete[] 表达式

销毁动态创建的对象,并释放其内存

int *p = new int{42};
delete p;
int *a = new int[n];
delete[] a;
  • new 必须对应 deletenew[] 必须对应 delete[],否则是 undefined behavior
    • 违反下列规则的一律是 undefined behavior:
      • delete ptr 中的 ptr 必须等于某个先前由 new 返回的地址
      • delete[] ptr 中的 ptr 必须等于某个先前由 new[] 返回的地址
      • free(ptr) 中的 ptr 必须等于某个先前由 malloc, calloc, reallocaligned_alloc 返回的地址。
  • 忘记 delete:内存泄漏

new/delete vs malloc/free

C++ 的对象模型比 C 复杂得多,而 new/delete 也比 malloc/free 做了更多的事:

  • new/new[] 表达式会先分配内存,然后构造对象。对于类类型的对象,它可能会调用一个合适的构造函数
  • delete/delete[] 表达式会先销毁对象,然后释放内存。对于类类型的对象,它会调用析构函数

在 C++ 中,非必要不手动管理内存

  • 当你需要创建“一列数”、“一列对象”,或者“一张表”、“一个集合”时,优先考虑标准库容器等设施,例如 std::string, std::vector, std::deque (双端队列), std::list/std::forward_list (链表), std::map/std::set (红黑树), std::unordered_map/std::unordered_set (哈希表)
  • 当你需要动态创建单个对象时,应该优先考虑智能指针 (std::shared_ptr, std::unique_ptr, std::weak_ptr)
  • 只有在特殊情况下(例如手搓一个标准库没有的数据结构,并且对效率有极高的要求),使用 new/delete 来管理动态内存
  • 当你对于内存分配本身也有特殊的要求时,才需要使用 C 的内存分配/释放函数,但通常也是用它们来定制 newdelete
posted @ 2024-04-24 03:29  Coinred  阅读(11)  评论(0编辑  收藏  举报