GKxx
new
delete
练习:使用基于范围的 for 语句遍历一个字符串,将大写改为小写
for
for (char c : str) c = std::tolower(c);
这样写是不行的:for (char c : str) 相当于让 char c 依次成为 str 中的每个字符的拷贝,在 c 上修改不会影响 str 的内容。
for (char c : str)
char c
str
c
如同
for (std::size_t i = 0; i != str.size(); ++i) { char c = str[i]; c = std::tolower(c); }
for (char &c : str) c = std::tolower(c);
char &c 定义了一个绑定到 char 的引用,我们让 c 依次绑定到 str 中的每个字符。
char &c
char
for (std::size_t i = 0; i != str.size(); ++i) { char &c = str[i]; c = std::tolower(c); // effectively str[i] = std::tolower(str[i]); }
int ival = 42; int &iref = ival; ++iref; // ival becomes 43 iref = 50; // ival becomes 50 std::cout << ival << std::endl; // 50 std::cout << iref << std::endl; // 50 int &iref2 = iref; // Equivalent to int &iref2 = ival; // iref2 also bounds to ival.
可以写 Type &r,也可以 Type& r
Type &r
Type& r
r
Type &
Type& r1, r2, r3;
r1
Type &r1, &r2, &r3;
引用必须初始化(即在定义时就指明它绑定到谁),并且这个绑定关系不可修改。
普通的变量、数组、指针等都是对象
可以定义绑定到数组的引用:
int a[10]; int (&ar)[10] = a; ar[0] = 42; // a[0] = 42
可以定义绑定到指针的引用:
int *p; int *&pr = p; pr = &ival; // p = &ival;
但“引用”本身只是一个傀儡,是一个对象的别名。
不能定义绑定到引用的引用:
int i = 42; int &r = i; int & &rr = r; // No such thing!
同样地,指针指向的类型、数组的元素类型也不能是引用。
int &*pr = &r; // No such thing! int &ar[N]; // No such thing!
一个引用被声明(并初始化)之后,使用它就是在使用它所绑定到的对象。
std::string s; std::string &r = s; r.push_back('a'); // 如同 s.push_back('a'); r = "hello"; // 如同 s = "hello"; std::cout << r << '\n'; // 如同 std::cout << s << '\n'; std::string &r2 = r; // 如同 std::string &r2 = s; , r2 也被绑定到 s
void print_array(const int (&array)[10]) { // 只读,所以加 const for (int x : array) // 基于范围的 for 语句可以用来遍历数组! std::cout << x << ' '; std::cout << std::endl; } int a[10], ival = 42, b[20]; print_array(a); // Correct print_array(&ival); // Error! print_array(b); // Error!
这个 array 真的是(绑定到了)一个数组,而不是指向数组首元素的指针。
array
int[10]
const
定义一个函数,接受一个字符串,将其中的大写字母改为小写字母
void to_lower(std::string &str) { for (char &c : str) c = std::tolower(c); }
std::string hello = "Hello"; to_lower(hello); // std::string &str = hello;
定义一个函数,接受一个字符串,输出其中的大写字母。
void print_upper(std::string str) { for (char c : str) if (std::isupper(c)) std::cout << c; std::cout << std::endl; }
void print_upper(std::string str) { for (char c : str) if (std::isupper(c)) std::cout << c; std::cout << std::endl; } std::string s = some_very_long_string(); print_upper(s);
传参的过程中发生了形如 std::string str = s; 的拷贝初始化。如果 s 很长,这将非常耗时。
std::string str = s;
s
以引用的方式传参,避免拷贝:
void print_upper(std::string &str) { for (char c : str) if (std::isupper(c)) std::cout << c; std::cout << std::endl; } std::string s = some_very_long_string(); print_upper(s); // std::string &str = s;
但这样有问题:
const std::string hello = "Hello"; print_upper(hello); // Error
print_upper
最好的做法:传递 reference-to-const
void print_upper(const std::string &str) { for (char c : str) if (std::isupper(c)) std::cout << c; std::cout << std::endl; }
一个表达式在被使用时,有时我们使用的是它代表的对象,有时我们仅仅是使用了那个对象的值。
str[i] = ch
str[i]
ch = str[i]
一个表达式本身带有值类别 (value category) 的属性:它要么是左值,要么是右值
哪些表达式能代表一个实际的对象?
哪些表达式产生的仅仅是一个值?
*addr
a[i]
a + b
&val
cond1 && cond2
* “左值”和“右值”这两个名字是怎么来的?
在 C 中,左值可以放在赋值语句的左侧,右值不能。
但在 C++ 中,二者的区别远没有这么简单。
目前已经见过的返回左值的表达式:*p, a[i]
*p
特别地:在 C++ 中,前置递增/递减运算符返回左值,++i = 42 是合法的。
++i = 42
赋值表达式返回左值:a = b 的返回值是 a 这个对象。
a = b
a
a = b = c
赋值表达式返回左值:a = b 的返回值是 a 这个对象(的引用)。
a = (b = c)
b = c
右值仅仅代表一个值,不代表一个实际的对象。常见的右值有表达式执行产生的临时对象和字面值。
std::string fun(); // a function that returns a std::string object std::string a = fun();
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 类型的临时对象。
Type(args...)
Type
std::string(10, 'c')
std::string("hello")
int(x)
int
x
int ival = 42; int &iref = 42; // Error int &iref2 = ival; // Correct int &iref3 = ival + 42; // Error int fun(); int &iref4 = fun(); // Error
C++11 引入了所谓的“右值引用”,我们在介绍移动的时候再讲。一般来说,“引用”指的是“左值引用”。
int arr[10]; int &subscript(int i) { // function returning int& return arr[i]; } subscript(3) = 42; // Correct. int &ref = subscript(7); // Correct. ref bounds to arr[7]
类似于“指向常量的指针”(即带有“底层 const”的指针),我们也有“绑定到常量的引用”
int ival = 42; const int cival = 42; const int &cref = cival; // Correct const int &cref2 = ival; // Also correct. int &ref = cival; // Error: Casting-away low-level const is not allowed. int &ref2 = cref; // Error: Casting-away low-level const is not allowed. int &ref3 = cref2; // Error: Even though cref2 bounds to non-const ('ival'), // this is still casting-away low-level const.
一个 reference-to-const 自认为自己绑定到 const 对象,所以不允许通过它修改它所绑定的对象的值,也不能让一个不带 const 的引用绑定到它。(不允许“去除底层 const”)
指针既可以带顶层 const(本身是常量),也可以带底层 const(指向的东西是常量),但引用不谈“顶层 const”。
特殊规则:常量引用可以绑定到右值:
const int &cref = 42; // Correct int fun(); const int &cref2 = fun(); // Correct int &ref = fun(); // Error
当一个常量引用被绑定到右值时,实际上就是让它绑定到了一个临时对象。
void print_upper(std::string &str) { for (char c : str) if (std::isupper(c)) std::cout << c; std::cout << std::endl; } print_upper("Hello"); // Error
当我们传递 "Hello" 给 std::string 参数时,实际上发生了一个由 const char [6] 到 std::string 的隐式转换,这个隐式转换产生右值,无法被 std::string& 绑定。
"Hello"
std::string
const char [6]
std::string&
将参数声明为常量引用,既可以避免拷贝,又可以允许传递右值
void print_upper(const std::string &str) { for (char c : str) if (std::isupper(c)) std::cout << c; std::cout << std::endl; } std::string s = some_very_long_string(); print_upper(s); // const std::string &str = s; print_upper("Hello"); // const std::string &str = "Hello";, Correct
将参数声明为常量引用,既可以避免拷贝,又可以允许传递右值,也可以传递常量对象,也可以防止你不小心修改了它。
在 C++ 中声明函数的参数时,尽可能使用常量引用(如果你不需要修改它)。
(如果仅仅是 int 或者指针这样的内置类型,可以不需要常量引用)
(语言律师需要掌握)
C++ 中的表达式依值类别被划分为如下三种:
lvalue + xvalue = glvalue(广义左值),xvalue + prvalue = rvalue(右值)
动态分配内存,并构造对象
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
对于内置类型:
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 异常
delete[]
销毁动态创建的对象,并释放其内存
int *p = new int{42}; delete p; int *a = new int[n]; delete[] a;
违反下列规则的一律是 undefined behavior:
delete ptr
ptr
delete[] ptr
free(ptr)
malloc
calloc
realloc
aligned_alloc
free
C++ 的对象模型比 C 复杂得多,而 new/delete 也比 malloc/free 做了更多的事:
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