一、类型体系与值类别
(一)全新的类型体系
先看一下C++的类型体系
这和C语言的类型体系像又不同,以下是C语言的类型体系
C语言另外还有类型组,比如把char和整型放在一起 ,构成了C语言的类型体系,与C++相比有点混乱,并且原始。
C++还提供了一元类型特性(UnaryTypeTrait),用于在编译时提供关于类型特性的信息。这些特性包括但不限于类型是否为某个特定类型、是否满足某种属性等,下面是一些一元类型特性
名称 | 描述 |
---|---|
std::is_void<T> | 判断类型T是否是void类型。 |
std::is_integral<T> | 判断类型T是否是整数类型(如int, char, bool等)。 |
std::is_floating_point<T> | 判断类型T是否是浮点类型(如float, double, long double)。 |
std::is_array<T> | 判断类型T是否是数组类型。 |
std::is_pointer<T> | 判断类型T是否是指针类型。 |
std::is_reference<T> | 判断类型T是否是引用类型。 |
std::is_member_pointer<T> | 判断类型T是否是成员指针类型。 |
std::cout << "Is Fundanmental " << std::boolalpha << std::is_fundamental<int*>::value << std::endl;
std::cout << "Is integral " << std::is_integral<int>::value << std::endl;
下面是一个官方的类型判断流程图
一元类型特性,涉及C++的高级特性,不多展开。但是从中我们可以发现,C++提供有对类型的操作,相较于C只有极少地方能操作类型,C++将其扩大化了,这会带来很多复杂性的问题,我们后面讨论。
(二)更细致化的值类别
Each C++ expression (an operator with its operands, a literal, a variable name, etc.) is characterized by two independent properties: a type and a value category.
在 C++ 中,每一个表达式(包括带有操作数的运算符、字面值、变量名等等)都具有两个相互独立的特性:类型和值类别。
在C中,我们只有左值(lvalue)和右值(rvalue) ,但是在C++中,我们有
1、广义左值
glvalue (“generalized” lvalue) is an expression whose evaluation determines the identity of an object or function;
广义左值,其求值能确定一个对象或函数的标识的表达式。
C++11引入的概念,将lvalue和xvalue统一,表示可以用来获取一个对象的值或地址的表达式是一个包含左值和将亡值的集合。广义左值是有“身份”的表达式,这使它区别于右值。这个分类的存在主要是为了提供一个区分哪些值可以被赋值,哪些值可以绑定到左值引用上的依据。
2、纯右值
prvalue (“pure” rvalue) is an expression whose evaluation computes the value of an operand of a built-in operator (such prvalue has no result object), or initializes an object (such prvalue is said to have a result object).
纯右值,是指其求值能计算内置运算符的操作数的值(此类 prvalue 没有结果对象),或者能初始化一个对象(此类 prvalue 被认为具有结果对象)。
即 纯右值指的是临时对象,它们没有持久的存储位置,通常在表达式中作为右值存在。
它们不能被赋值,不能出现在赋值的左侧,它表示的是数据本身,而不是数据的地址。
- 这些值通常是不可修改的,因为它们不存在于内存中的某个位置。
- 纯右值是临时性的,一旦被使用,其生命周期就结束了。
内置类型操作符的运算结果:当一个内置类型的操作符(如算术操作符、位操作符等)被评估时,其结果是一个prvalue。这种prvalue没有与之关联的结果对象(result object),因为它直接表示了操作的结果值。
int a = 5;
int b = 10;
int c = a + b; // 这里,a + b 的结果是一个prvalue
初始化对象的表达式:当一个表达式用于初始化一个对象时,该表达式被视为prvalue,并且此时它有一个结果对象。这个结果对象可能是变量、通过new
表达式创建的对象、通过临时物质化(temporary materialization)创建的临时对象,或者是这些对象的成员。
std::string s = std::string("hello") + " world";
// 字符串字面量与std::string相加产生一个临时std::string对象
最简单的,除了字符串的所有字面量都属于纯右值,纯右值不与具体对象绑定,所以不能取地址,可以使用&检验。
3、将亡值
xvalue (an “eXpiring” value) is a glvalue that denotes an object whose resources can be reused;
将亡值,广义左值的一种,它表示其资源可被重用的对象。
将亡值是C++11中引入的概念,最常见的将亡值是返回右值引用的函数或方法,将亡值是指那些即将被销毁或离开作用域的对象,它们可以被移动,但不能被复制。
将亡值是暂时性对象或变量,比如作为运算的结果对象,即将被销毁,它们的资源,比如值可被复用。
4、左值
lvalue is a glvalue that is not an xvalue;
左值,广义左值的一种,但不是将亡值
左值指的是那些有“身份”的表达式,即可以获取其内存地址并且可以安全使用该地址的值左值可以出现在赋值的左侧或右侧。
例如:变量名、数组名、指针的解引用等。
5、右值
rvalue is a prvalue or an xvalue;
右值,是一种纯右值或者将亡值
多增加的几种值类别是为了支持C++的高级特性——(移动语义) ,所有表达式属于纯右值、将亡值和左值三种之一,它们是最基本的值类别。
二、引用
在C++类型体系中,引用(reference)是唯一在C的类型体系没有直接关联性的类型。来一个
int a = 12138;
int& b = a;
std::cout << "a = " << a << std::endl;
std::cout << "b = " << b << std::endl;
在这个例子中,我们说 是
的一个引用,显然,这个例子,一目了然,能看出引用有什么作用。我们先不继续谈论引用,让我们看看在引用以前,我们有什么
(一)引用以前
看一个非常熟悉的程序
void swap(int a, int b){
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
我们知道这个程序,无法如期交换两个整数,为了顺利交换,我们应该
void swap(int * a, int * b){
if(a && b){
*a = *a ^ *b;
*b = *a ^ *b;
*b = *a ^ *b;
}
}
一切正常,但是,如果我们想要交换两个整数指针,我们需要
void swap(int** a, int** b){
int * temp ;
if(a && b && *a && *b){
temp = *a;
*a = *b;
*b = temp;
}
}
如果是交换两个二级整数指针,显然,我们需要更多的工作。即使,确保说,我们将不会进行多级指针的交换,我们也无法确保指针指向了正确的对象。
一个不恰当比喻,我们说,有一台发电机,它在一个房子里,只有A知道,B需要发电机发电到他家里,于是有两种情况
情况一,他知道A的地址,所以找到A,说,给我家发电吧;
情况二,他不知道A的地址,于是需要一个知道他地址的人,姑且,我们叫他C,C说,我知道,不过这需要点成本。于是,B花费了成本得到了一个地址,B按照地址结果到了一块墓地,无论后续如何,沉没成本已经发生。可能C给的地址也是别人给的,或者这地址是别人从别人那里得到的……,除非亲自检验,否则没人知道地址是否正确。
指针很好,除非你掌握它。
(二)引用如何解决问题
声明为type&的变量是声明为type的变量的替代名称。当我们声明一个引用变量时,不会在内存中创建一个新的对象,而只是声明一个现有变量的替代名称。原始变量和引用变量实际上是由不同名称调用的相同内存位置。
因此,我们需要在声明引用变量时立即将其绑定到原始变量。绑定是通过使用原始变量的名称初始化引用变量来完成的。
由于,原始变量和引用变量,所以,这里并不存在“造假”,类似于一个人和其身份证是相互绑定的,不存在有身份证号,没有人。引用变量将优雅解决我们的交换问题
void swap(int & a, int & b){
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
1、永久绑定
在引用变量被声明并绑定到变量名之后,它们之间就创建了引用关系,并且在变量被销毁(超出作用域范围) 之前无法断开引用关系。
在C++语言中,我们说变量和相应的引用变量之间的关系是一个恒定关系。在创建引用关系之后,不能更改引用关系。
这是一个有点让人困惑的说法,考虑下面的程序
int a = 5;
int& ra = a;
int b = 10;
ra = b;
引用变量有无改变其原来的引用关系呢,如果输出
的值,那么无论是改变了,还是仅仅是赋值操作,它的值都是
,最好的方法是通过地址
可见并没有,它们只是单纯的赋值操作。同时看另一个程序
int x { 5 };
{
int& ref { x }; // ref is a reference to x
std::cout << ref << '\n'; // prints value of ref (5)
} // ref is destroyed here -- x is unaware of this
std::cout << x << '\n'; // prints value of x (5)
所以,永久绑定对,也不对。确切的说,无论是原始变量还是引用变量都是同一块内存的名字,少一个名字,多一个名字对内存本身没有影响,除非通过名字对内存本身造成影响。
这里有一个特殊的情况,即被引用量本身在引用给引用变量之前被消亡了,这可能发生在函数引用返回的情况下,我们称之为“悬空引用”
正常情况下,我们并不这样做,所以,不多讨论。
注:
没有必要在同一个名称空间中(例如在同一个函数中)使用引用, 因为我们总是可以使用原始变量而不需要使用引用变量。
另外需要提醒的是,引用变量必须初始化,除非是形参,原因很简单,它不是独立的变量
2、引用限制
一个引用关系中只有一个值,可以通过原始变量或者任何引用变量修改该值,除非我们使用const修饰符。
下表为对原始变量和引用变量使用const修饰符的情况
说明:
- 情况一,通过原始变量或者引用变量更改值没有限制
- 情况二,因为我们试图将一个非常量引用变量绑定到一个常量变量。由于原始变量已经是常量,因此无法通过引用变量更改该值。
- 情况三,可以通过数据变量更改数据,但我们希望通过引用变量限制数据的更改。
- 情况四,我们希望创建一个原始变量和一个引用变量,但原始变量和引用变量都不能更改公共的值。由于数据变量和引用变量只能用于检索数据,而不能用于更改数据,因此本例的应用很少见。
4、引用的引用?
我们知道,对于指针,我们可以
int a;
int * pa = &a;
int ** ppa = pa;
那么,引用是否具有这样的特性,我们可以试试
显然,不能,让我们分析我们做了什么,首先我们创建了一个整型变量,又创建了一个引用变量
,它引用了
,换句话说,
还是
,所以以某种角度而言, 叠加&没有意义,为了赋予其语义,C++可以这样
int && a = 1;
这有点奇怪,这样的变量定义有什么作用? 因为我们完全可以
int a = 1;
这就涉及了两个概念左值引用和右值引用
。这两个概念很容易理解
int a = 10;
int& rRef = a; // 正确,左值引用绑定左值
//int&& rrRef = a; // 错误,右值引用绑定左值
//int& rRef2 = 1; // 错误,左值引用绑定右值
int&& rrRef2 = 1; // 正确,右值引用绑定右值
至于,为什么是引用右值,我们引入另一个概念,先看一个官方例子:
typedef int& lref;
typedef int&& rref;
int n;
lref& r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref& r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&
这叫做引用坍缩(Reference collapsing)
rvalue reference to rvalue reference collapses to rvalue reference, all other combinations form lvalue reference
右值引用对右值引用的组合会坍缩为右值引用,而其他所有的组合都会形成左值引用
注意,引用符号在字面上只能存在两个, 这在C语言中只叫做逻辑与运算符,但是C++赋予了其新语义。
不恰当的比喻,把指针理解为“嵌套”操作,地址就像一个箱子,最里面是值,每多一个Asterisk(*),我们就套一个箱子,引用就是开箱子,一个代表最里面的箱子,再一个
就是值了。这与在C语言中就有的解引用有异曲同工之处。
References are not objects; they do not necessarily occupy storage, although the compiler may allocate storage if it is necessary to implement the desired semantics (e.g. a non-static data member of reference type usually increases the size of the class by the amount necessary to store a memory address).
引用不是对象;它们不一定占用存储空间,尽管编译器可能会分配存储空间,如果这对于实现所需的语义是必要的(例如,引用类型的非静态数据成员通常会使类的大小增加存储内存地址所需的量)。
Because references are not objects, there are no arrays of references, no pointers to references, and no references to references
因为引用不是对象,所以不存在引用数组、指向引用的指针以及引用的引用
5、关于引用初始化
实际上,引用的初始化并不像上面那样简单,详细的初始化规则,相对复杂一点,涉及到更高级的特性,这里不讨论。
这里的初始化针对非列表初始化,否则运用的是列表初始化规则。
下面通过一些例子来分析一下,先介绍两个术语,对于给定的两个类型T1和T2
Given the cv-unqualified versions of T1 and T2 as U1 and U2 respectively, if U1 is similar to U2, or U1 is a base class of U2, T1 is reference-related to T2.
如果在去掉各自的cv修饰符后,U1和U2是相似的(简单来说一样),或者其中一个是另一个的子类,则两者引用关联
If a prvalue of type “pointer to T2” can be converted to the type “pointer to T1” via a standard conversion sequence, T1 is reference-compatible with T2.
如果指向T2的指针类型可以通过标准转换序列(不强制转换)转换为指向T1的指针类型,则两者引用兼容
另外下面是一个规则说明参考
对于引用初始化模板
T& ref = object;
T&& ref = object;
给定以下变量
int i = 1;
const int ci = 1;
short s = 1;
long l = 2;
double d = 3.14;
int* p = &i;
// 左值引用,正常情况下,绑定左值,直接绑定
int& ri1 = i; // 直接绑定
const int& cri1 = i; // 增加cv限定符,绑定左值
const volatile int& cri2 {i}; // 不影响绑定,也支持其它初始化语法
// int& cri3 = ci; // 错误, 非const左值不能绑定const左值引用
int& ri2 = const_cast<int&>(ci); // 强制类型转换,绑定左值
有const限定符的左值引用,可以绑定右值和非引用关联的对象
隐式转换成一个类型是“无 cv 限定的 T” 的纯右值。然后应用临时量实质化,在将该纯右值的类型视为 T 的情况下将引用绑定到结果对象。
const int& cri4 = 10; // 10 是一个纯右值,绑定到临时对象,绑定到cri4
const int& cri5 = 'i'; // char类型转换为int类型
const int& cri6 = s; // short转换为int类型
const int& cri7 = l; // long类型转换为int类型
const int& cri8 = d; // double类型转换为int类型
//const int& cri9 = p; // 指针类型不能转换为int类型
可以证明
// 右值引用,基本属于间接绑定,只要目标是右值
int && rri1 = 0; // 纯右值, int类型
int && rri2 = 'a'; // 纯右值, char类型, 转换为int类型
int && rri3 = d; // double类型
int && rri4 = static_cast<int&&>(i); // 使用static_cast运算符
int && rri5 = const_cast<int&&>(ci); // 去掉const限定符,绑定到右值
int && rri6 = i + 1; // 表达式可以产生xvalue
//int && rri7 = p; // 错误, 指针类型无法进行隐式转换
int && rri7 = static_cast<int&&>(*p);// 指针解引用,转换为int类型,临时对象
const int && crri1 = 'a'; // 纯右值, char类型
const int && crri2 = d; // double类型
const int && crri3 = static_cast<int&&>(i); // 使用static_cast运算符, 更少的cv限定符
const int && crri4 = static_cast<const int&&>(i); // 相同的cv限定符
const int && crri5 = i + 1; // 表达式可以产生xvalue
//const int && crri6 = p; // 错误, 指针类型无法进行隐式转换
const int && crri6 = static_cast<const int&&>(*p); // 指针解引用,转换为int类型,临时对象
以上引用初始化特性,会在函数重载上表现,比如加const修饰的引用类型和不加const修饰的引用会认为是不一样的类型,这和其它类型不同
More importantly, when a function has both rvalue reference and lvalue reference overloads, the rvalue reference overload binds to rvalues (including both prvalues and xvalues), while the lvalue reference overload binds to lvalues
更为重要的是,当一个函数同时具有右值引用和左值引用的重载版本时,右值引用的重载会绑定到右值(包括纯右值和将亡值),而左值引用的重载则会绑定到左值。
三、指针
(一)为什么还需要指针
现在,有了引用,一些可能涉及复杂指针操作的场景,我们可以使用引用来替代,那么我们还需要指针来做什么?
其实再更复杂一点的程序,就可以得到答案
typedef int Array[];
typedef int Array3[3];
typedef int Array4[4];
int a[] = {1, 2, 3};
int b[] = {1, 2, 3, 4};
// int[3] & rArr1 = a; // 类型声明错误
// Array & rArr = a; // 类型转换错误int[3] 不能转换为int[]
Array3 & rArr2 = a;
const Array3 & rArr3 = a;
rArr2[0] = 90;
// rArr3[0] = 90; // 错误,const引用不能修改
// Array4 & rArr4 = a; // 类型转换错误int[3] 不能转换为int[4]
Array4 & rArr5 = b;
int * pArr = a;
int * pArr2 = b;
引用对于类型太过于“专一”,在某些情况下,特别是在类型宽松的场景中,表现不如指针,另外,对于需要比较大内存空间的程序,往往会涉及到堆内存,而这引用无法作用,对于函数类型也是如此。
(二)学会看指针类型
指针是需要的,所以学会看指针的类型也是必要的,有一个相当好用的方法:right-left rule
方法如下
从标识符开始,写下标识符 is,先往右边解析,如果碰到不完整的以下符号或者两端就反向,如果连续碰到的不完整符号配对,则这两个不完整符号消除,对于碰到的符合下表的符号写下其转换
符号 | 转换 | 意思 |
---|---|---|
* | pointer to | 指向... 的指针 |
[] | array of | ... 的数组 |
[n] | array(size n) of | 大小为...的数组 |
() | function returning | 返回.... 的函数 |
(type, ...) | function expecting (type, ...) and returning | 接收...参数并返回...的函数 |
type | type | 类型 |
试一试,第一组,数组相关
int *p[];
// p is array of pointer to int
// p 是数组, 元素为 指针(指向int类型)
int *p[3];
// p is array (size 3) of pointer to int
// p 是 数组(大小为3),元素为指针(指向int类型)类型
int (*p)[];
// p is pointer to array of int
// p 是 指针,指向数组,元素为int类型
int (*p)[3];
// p is pointer to array (size 3) of int
// p 是 指针,指向数组(大小为3),元素为int类型
第二组,函数相关
int p();
// p is function returning int
// p 是 函数, 返回int 类型
int p(int, float);
// p is function expecting (int , float) and returning int
// p 是 函数,接受int和float类型并返回int类型
int * p(int, float);
// p is function expecting (int, float) and returning pointer to int
// p 是 函数,接受int和float类型并返回 指针(指向int类型)
int (*p)();
// p is pointer to function returning int
// p 是 指针 指向 返回 int类型的 函数
int (*p)(int , float );
// p is pointer to function expecting (int, float ) and returning int
// p 是 指针 指向 返回int类型的 接受int和float参数的 函数
int (*p[])(int, float);
// p is array of pointer to function expecting (int, float) and returning int
// p 是 指针数组, 元素为 指向 接受int和float类型 并 返回int类型的 函数 的指针
int (*p[3])(int, float);
// p is array (size 3) of pointer to function expecting (int, float) and returning int;
// p 是 指针数组(大小为3), 元素为 指向 接受int和float类型 并 返回int类型的 函数 的指针
int (*p())(int, float);
// p is function returning pointer to function expecting (int, float) and returning int
// p 是函数,返回指针(指向接受int和float类型 并 返回int类型的函数)
拿一个实际的函数原型,练练手
void (*signal(int signo, void(*handle)(int)))(int)
// signal is function expecting (int, void(*handle)(int)) and returning pointer to function expecting int and returning void
// signal 是 函数,接受(int, void(*handle)(int))并且返回指针(指向接受int类型并且返回void的函数)类型
反向的,我们可以创建一个想要的指针类型,比如一个指针,它指向一种函数A,它接受一个int类型,返回一个函数指针,这种函数B返回int类型,接受两个int类型
首先,它是指针
再者,接受一个int类型,并返回一个函数指针
是一个函数指针,
实现如下
#include <iostream>
typedef int (*FuncPtr)(int, int);
FuncPtr (*p)(int) = nullptr;
int add(int a, int b) {
return a + b;
}
FuncPtr getFunc(int i) {
if (i == 1) {
return add;
}
return nullptr;
}
int main() {
std::cout << "Add 3 + 4 = " << getFunc(1)(3, 4) << std::endl;
}
(三)内存管理
指针操作是内存地址的操作,而程序的基本空间不是无限的,在C语言中,我们有内存的分区,以实现程序资源的统一管理,在C++中,我们也存在,不过,这里主要讨论内存的分配方式
1、内存分配方式
内存分配主要有三种方式:静态存储分配、自动存储分配(栈)、动态存储分配(堆)。
静态内存分配 适用于静态变量和全局变量。这些类型变量的内存会在程序运行时一次性分配,并在程序的整个生命周期中持续存在。
自动内存分配 适用于函数参数和局部变量。这些类型变量的内存会在相关块进入时分配,并在块退出时释放,根据需要多次进行。
动态内存分配 适用于复杂类型和“动态”变量。这些类型变量的内存由程序员手动申请并手动释放。
在C语言中,这三种内存方式存在一些局限
- C没有提供额外的机制来管理静态和动态存储分配
- 静态变量和全局变量可以随意定义,显得很杂乱
- 动态内存分配机制,相对原始,依据字节语义而不是类型语义去分配空间
所以,C++对这些做了一些修正,下面主要先介绍动态内存分配
2、智能内存管理
在C语言中,我们经常使用mallo或者calloc来申请堆空间,但是这存在很多问题
存在类型转换隐患
#include <stdio.h>
#include <stdlib.h>
typedef int Array[10];
int main() {
int* a = malloc(sizeof(10) * 9);
Array b ;
a = b;
return 0;
}
对于类类型表现不好
struct Student{
std::string name;
int age;
float gpa;
Student():name("John Doe"), age(18), gpa(3.5){
std::cout << "Default constructor called" << std::endl;
}
~Student(){
std::cout << "Destructor called" << std::endl;
}
void print(){
std::cout << "name: " << name << std::endl;
std::cout << "age: " << age << std::endl;
std::cout << "gpa: " << gpa << std::endl;
}
};
int main() {
Student* s = (Student*)malloc(sizeof(Student));
s->print();
}
所以,我们需要新的,更智能化的堆内存分配方法
new和delete运算符是C++比较底层的内存分配操作符
分配数组 释放对象 释放数组 | new type [size] delete ptr delete [ ] ptr |
- 第一个运算符用于在堆中为单个对象分配内存。
- 第二个运算符用于在堆中创建对象数组。
- 第三个运算符用于使用指针删除单个对象。
- 第四个运算符用于删除为堆中的数组分配的内存。
4、new 运算符
new
表达式尝试申请存储空间,并在已申请的存储空间上,尝试构造并初始化为一个无名对象,或无名对象的数组。new表达式返回一个指向所构造的对象或者对象数组的纯右值指针。
new表达式的语法如下
看起来,有点复杂,不过在很多时候都比较简单,下面分段介绍
初始化
// 默认初始化
int *pInt = new int;
// 分配一个int类型的内存空间,并返回一个指向该内存空间的指针
// 直接初始化
int *pInt2 = new int(10);
// 分配一个int类型的内存空间,并初始化为10,并返回一个指向该内存空间的指针
// 列表初始化
int *pInt3 = new int{10};
// 分配一个int类型的内存空间,并初始化为10,并返回一个指向该内存空间的指针
不同于C语言,C++支持在分配堆内存时指定内存的值,在C语言中只能清零
类型与转换
// 可以使用auto关键字,自动通过初始化表达式推断类型
int *pInt4 = new auto(10);
// 分配一个int类型的内存空间,并返回一个指向该内存空间的指针
// 指针类型转换问题,不同的指针类型之间不能直接进行转换
int *pInt5 = reinterpret_cast<int *>(new short);
// 分配一个int类型的内存空间,并返回一个指向该内存空间的指针
int *pInt6 = reinterpret_cast<int *>(new long);
// 分配一个long类型的内存空间,并返回一个指向该内存空间的指针
不同数据类型,虽然能够通过强转通过,但是显然是有内存风险的
定位new
new运算符支持传入额外的参数用于构造对象,这样的new表达式叫做
int *pInt7 = new (std::nothrow) int(1);
// 分配一个int类型的内存空间,并返回一个指向该内存空间的指针,如果分配失败,则返回nullptr。
// 这里的std::nothrow参数表示,如果内存分配失败,则返回nullptr,而不是抛出异常。
int *pInt8 = new (pInt7) int(1);
// 在已经分配好的内存空间上构造一个int类型的对象,并返回一个指向该对象的指针。
// 这里的pInt7参数表示,在pInt7指向的内存空间上构造一个int类型的对象。
int *pInt9 = new (std::align_val_t(16)) int(1);
// 分配一个int类型的内存空间,并返回一个指向该内存空间的指针,要求内存地址对齐到16字节。
// 这里的std::align_val_t参数表示,要求内存地址对齐到指定的字节数。
复杂的细节以后说明
分配数组
以上的初始化,只用于初始化单个对象,当想要初始化多个对象,就要使用运算符
int * arr1 = new int[10];
// 分配一个int类型的数组,包含10个元素,初始化值个数可以任意,
// 只要在数组大小之内,初始值会依次赋给数组元素
int * arr2 = new int[10]{1, 2, 3, 4, 5, 6, 7, 8, 9};
// 不能使用auto关键字,因为auto关键字无法推断数组的元素类型
// int * arr3 = new auto[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 不支持C99的指定器语法
//int * arr4 = new int[3]{[0] = 1, [1] = 2, [2] = 3};
5、delete运算符
delete运算符相对简单
可以结合以下说明理解
对于第一种(非数组)形式,表达式 必须是指向对象类型的指针或可按语境隐式转换到这种指针的类类型,且其值必须为空 (null) 或指向 new 表达式所创建的非数组对象的指针,或指向 new 表达式所创建的对象的基类子对象的指针。若 表达式 为其他值,包括它是通过new 表达式的数组形式获得的指针的情况,其行为未定义。
对于第二种(数组)形式,表达式 必须是空指针值或先前由 new 表达式的数组形式所获得的指针值。若 表达式 为其他值,包括若它是由 new 表达式的非数组形式获得的指针的情况,其行为未定义。
下面来看一些有趣的情况
int a ; // 删除非指针
delete a; // 错误,编译器、智能的IDE都会报错
int * b; // 删除没有被分配堆空间的指针
delete b; // 编译器和IDE都会通过
int c; // 删除指向栈地址的指针
delete &c; // 编译器和IDE都会通过
加上一些输出,看看实际结果
好像,非常聪明地为我们“免”删除了一些对象,但是如果在IDE中,你就会发现,并不是这样简单,在Clion运行没有输出结果,一闪而过了错误后,显示了退出异常。
为什么呢?这里先留个疑问