第一章:C++和标准库速读

书接上文

条件语句

       条件语句可以根据某事是否为真来执行代码。如下面的小节中所展示的,C++中有三种主要的条件语句类型:if/else语句,switch语句和条件运算符。

if/else语句

       最常用的条件语句是if语句,它可以跟else搭配使用。如果if语句中的条件为真,一行或一块代码被执行。否则,如果else存在的话就执行它,或都执行条件语句后面的代码。下面的代码示例了一个级联的if语句,一个有趣的理解是这个if语句带着一个else语句,else语句反过来又包含另一个if语句,依次类推:

if (i > 4) {
    // Do something.
} else if (i > 2) {

    // Do something else.
} else {
    // Do something else.
}

        if语句括号中的表达式必须是一个布尔值或者可以计算出一个布尔值。0值为false,非0值为true。例如:if(0)等价于if(false)。稍后介绍的逻辑运算符提供了一种将表达式计算为true或false布尔值的一种方法。

条件语句初始化设置

C++17 使用下面的语法可以在条件语句里面提供一个初始化设置:

if (<initializer> ; <conditional_expression>) { <body> }

          <initializer>中的变量只在 <conditional_expression>和 <body>中有效。它们在条件语句之外是无效的。在本书中给出这个特性的有用示例还为时过早,但下面是它的样子:

if (Employee employee = GetEmployee() ; employee.salary > 1000) { ... }

        这个例子中,初始化设置获取一个雇员,条件语句判断他的薪水是否超过1000。只有在大于1000的情况下条件语句的代码块才会被执行。本书会给出更多具体的例子。

switch语句

            switch语句是另一种根据表达式的值执行操作的语法。在C++中,switch语句中表达式的值必须为整型、可转换为整型的类型、枚举类型或者强枚举型,并具必须与常量进行比较。每一个常量值代表一个''case"。如果表达式与case匹配,则接下来的语句会被执行,直到一个break语句为至。也可以提供一个默认的case,如果其它case没有匹配的话就匹配到这个。下面的伪代码展示了switch语句的常见用法:

switch (menuItem) {
    case OpenMenuItem:
        // Code to open a file
    break;
    case SaveMenuItem:
        // Code to save a file
    break;
    default:
        // Code to give an error message
    break;
}

           switch语句通常可以转换成if/else语句。上面的switch语句可以转换成下面的形式:

if (menuItem == OpenMenuItem) {
    // Code to open a file
} else if (menuItem == SaveMenuItem) {
    // Code to save a file
} else {
    // Code to give an error message
}

         通常是在基于表达式的多个特定值,而不是对表达式的某种测试,进行操作的情况下使用switch语句。这种情况下,switch语句可以替代嵌套的if-else语句。如果是测试单个值,那么if或者if-else语句是最好的选择。
        一旦找到一个与表达式匹配的case,那么它下的所有语句被执行直到一个break语句结束。即使遇到另一个case表达式(称为fallthrough),也会继续执行。下面的例子中有几个不同的case匹配时都会执行代同一个代码块:

switch (backgroundColor) {
    case Color::DarkBlue:
    case Color::Black:
        // Code to execute for both a dark blue or black background color
    break;
    case Color::Red:
        // Code to execute for a red background color
    break;
}
        滑落(Fallthrough)会成为bugs的地方,例如恰巧忘记写break语句。因此编译器在switch语句中遇到一个滑落(Fallthrough)时会给出一条警告,除非像上面的例子case是空的。从C++17开始,可以使用[[fallthrough]]属性来告诉编译器这个滑落是特意的,如下:
switch (backgroundColor) {
    case Color::DarkBlue:
        doSomethingForDarkBlue();
    [[fallthrough]];
    case Color::Black:
        // Code is executed for both a dark blue or black background color
        doSomethingForBlackOrDarkBlue();
    break;
    case Color::Red:
    case Color::Green:
        // Code to execute for a red or green background color
    break;
}

switch语句初始化器(Initializers for switch Statements)

         正如if语句,C++17也添加了对switch语句初始化器的支持。语法如下:
switch (<initializer> ; <expression>) { <body> }

          <initializer> 中的变量只在<expression>和<body>中有效。在switch语句之外,它们是无效的。

        条件运算符
 
        C++中有一个带三个参数的运算符,称为三元运算符。它是形如“if [ something] then [ perform action], otherwise [ perform  some other action].”条件表达式的简写。用?和:表示条件操作。下面代码中的变量i如果大于2则输出"yes",否则输出"no"。
 
std::cout << ((i > 2) ? "yes" : "no");

        i > 2的括号是可选的,所以下面是等价形式:

std::cout << (i > 2 ? "yes" : "no");

        条件运算符的优势是几乎可以在任何上下文中使用。前面的例子中,条件运算符是在输出环境中使用。一种简便记住它的使用方法是将问号看作它前面的语句确定是一个问题。例如,“i是否大于2?如果是,则结果是'yes';否则结果是'no'。"

        与if语句或switch语句不同的是条件运算符不依赖结果执行代码块。相反,它的使用正如前面的例子。从这种角度看,它确实是一个运算符(像+和-),而不是像if和swich那样是直正的条件语句。

 
       逻辑运算符
 
         你已经看过非正式定义的逻辑运算符了。>运算符比较两个值。如果左边的值大于右边的则结果为“true”。所有的逻辑运算符都遵循这个模式——它们的结果只有真和假。下表展示了常用的逻辑运算符:
 
运算符描述用法

<

<=

>

>=

检验左边是否小于,小于或等于,大于,大于或等于右边
if (i < 0) {
      std::cout << "i is negative";
}
==
检验左边是否等于右边。不要和=(赋值)运算符混淆!
if (i == 3) {
      std::cout << "i is 3";
!=
不相等。如果左边不等于右边则结果为真。
if (i != 3) {
      std::cout << "i is not 3";
}
!
逻辑非。对布尔表达式的真/假状态取反。这是一个一元运算符。
if (!someBoolean) {
      std::cout << "someBoolean is false";
}
&&
逻辑与。如果表达式的两个部分都为真是结果为真。
if (someBoolean && someOtherBoolean) {
      std::cout << "both are true";
}
||
逻辑或。如果表达式的任何部分为真结果就为真。
if (someBoolean || someOtherBoolean) {
      std::cout << "at least one is true";
}
        C++计算逻辑表达式时使用短路逻辑。这意味着最终结果一旦确定,那么表达式的剩余部分就不会再计算了。例如,对多个布尔表达式进行逻辑或运算,如下所示,只要有一个为真,那么最终结果就为真。不会再检验剩余的部分。
bool result = bool1 || bool2 || (i > 7) || (27 / 13 % i + 1) < 2;

        这个例子中,如果bool1是真,那么整个表达式一定为真,所以其它部分就不被计算。通过这种方式,C++语言就可以避免程序运行不必要的代码。当然,如果后面的表达式在某种程度上影响程序的状态(例如,对过调用一个单独的函数),那么这可能成为难以寻找bugs的地方。下面的代码示例了一个使用&&的语句,由于第二项导致短路,因为0的计算结果始终为假。

bool result = bool1 && 0 && (i > 7) && !done;

         短路对性能是有好处的。可以将速度快的测试放到前面这样当逻辑短路发生时耗时的测试就不会被执行。在有指针的环境下也很有用,当指针非法时可以避免表达式其它部分被执行。本章后面会讨论指针和指针短路。

        函数
 
        对于任何大型程序,将所有的代码放到main()中都是无法管理的。为了使程序容易理解,需要将程序的代码分解到精简的函数中。在C++中首先声名函数以使它可在其它代码中使用。如果函数只在特定的文件中使用,通常在源文件中声明和定义它。如果函数是为其它文件或模块使用,则通过把声明放在头文件中,定义放在源文件中。
 
        注意 函数声名通常称为函数原型或者函数头以强调他们代表函数的使用方式,而不是函数背后的代码。术语函数签名表示的是函数名称和函数参数列表的组合,不包括返回类型。
 
       下面的代码展示了函数声名。这例子中有一个void返回类型,表明函数不向调用者返回结果。调用者必须提供两个参数来调用函数——一个整型和一个字符型。
void myFunction(int i, char c);

        没有对应于函数声名的实际定义,在编译的链接阶段会出错,因为使用此函数的程序在调用不存在的代码。下面的定义打印两个参数的值:

void myFunction(int i, char c)
{
    std::cout << "the value of i is " << i << std::endl;
    std::cout << "the value of c is " << c << std::endl;
}

        在程序的其它地方调用myFunction()并给两个参数输入值。一些简单的函数调用示例如下:

myFunction(8, 'a');
myFunction(someInt, 'b');
myFunction(5, someChar);

        注意 不像C,在C++中一个没有参数的函数仅仅有一个空参数列表。没有必要使用void来指明不需参数。然而,当没有返回值时仍然需要使用void来指示。

       C++函数也可以向调用者返回值。下面的函数把两个数相加并把结果返回:
int addNumbers(int number1, int number2)
{
    return number1 + number2;
}

        这个函数可以像下面这样调用:

int sum = addNumbers(5, 3);

        函数返回类型推导

        C++14中,你可以要求编译器来自动推导函数的返回类型。为使用这个特性,必须使用auto作为返回类型:
auto addNumbers(int number1, int number2)
{
    return number1 + number2;
}

        编译器根据返回语句的表达式来推导返回类型。函数中可以有多条返回语句,但它们应该都解析成相同类型。这样的函数甚至可以包含递归调用(自我调用),但是函数中的第一个返回语句必须是非递归调用。

        当前函数的名字

         每一个函数都有一个本地预定义变量__func__,它包含当前函数的名字。此变量的一个用途是日志输出:

int addNumbers(int number1, int number2)
{
    std::cout << "Entering function " << __func__ << std::endl;
    return number1 + number2;
}
        C风格数组
 
        数组保存一系列具有相同类型的值,每一个值都可以通过它在数组中位置进行访问。在C++中,声明数组时必须指定数据的大小。变量不能做为数组的大小——它必须是一个常量,或者常量表达式(constexpr)。常量表达式在11章讨论。下面的代码展示了有三个整型元素的数组声名,后面的三行代码将这些元素初始化为0:
int myArray[3];
myArray[0] = 0;
myArray[1] = 0;
myArray[2] = 0;

        警告 在C++中,数组的第一个元素的位置总是0,不是1。

        下一节介绍的循环可以用来初始化数组元素。但是,除了使用循环和前面的初始化机制外,也可以用下面的一行来将其初始化为0:
 
int myArray[3] = {0};

       甚至可以像下面这样不要0:

int myArray[3] = {};

        数组也可以使用初始化列表初始化,这种情况下编译器可以自动推断数组的大小。如下:

int myArray[] = {1, 2, 3, 4}; // The compiler creates an array of 4 elements.

        如果确实指定了数组大小,并且初始化列表的元素小于数组的大小,那么数组余下的元素被设置成0。例如,下面的代码只把数组的第一个元素设置为2,其它所有的元素都设置成0:

int myArray[3] = {2};

        在C++17中可以使用std::size()(需要<array>)函数来获取基于栈的C风格数组的大小。例如:

unsigned int arraySize = std::size(myArray);

        如果你的编译器还不支持C++17,获取基于栈的C风格数组大小的诀窍是使用sizeof运算符。sizeof运算符以字节单位返回其参数的大小。为了获取基于栈的数组元素数量,需要用以字节为单位的数组大小除以以字节为单位的第一个元素的大小。例如:

unsigned int arraySize = sizeof(myArray) / sizeof(myArray[0]);

        前面的例子展示了一维数组,可以想像成一行整数,每一个都有属于自己的带编号隔间。C++支持多维数组。可以把二维数组想像成一个棋盘,每一个位置都有一个x坐标和一个y坐标。三维和更高维很难图形化,并且使用的也很少。下面的代码展示了分配一个二维的字符数组来当井字板,然后在方形中央放一个“o”:

char ticTacToeBoard[3][3];
ticTacToeBoard[1][1] = 'o';

        图1-1显示了该板的可视化表示和每个正方形的位置。

 
TicTacToeBoard[0][0]
TicTacToeBoard[0][1]
TicTacToeBoard[0][2]
TicTacToeBoard[1][0]
TicTacToeBoard[1][1]
TicTacToeBoard[1][2] 
TicTacToeBoard[2][0]
TicTacToeBoard[2][1]
TicTacToeBoard[2][2]
        注意 在C++中最好避免使用本节讨论的C风格数组,并且使用标准库功能来替代,如接下来的两节讨论的std::array和std::vector。
 
 
        std::array
 
         上一节讨论的数组来源于C,在C++中依然使用。但是C++有一个固定大小容器的特殊类型叫做std::array,定义在<array>头文件中。它基本上是对C风格数组的轻量级封装。用std::arrays替代C风格数组有几项优势。它们总是知道自己的大小,不用自动转换成指针可以避免某些bugs,有迭代器可以方便的遍历元素。在17章讨论迭代器的细节。下面的例子示例数组容器的用法。array后面的尖括号(array<int, 3>)会在12章讨论模板时变得清楚。现在,我们只需要记住在尖括号里面必须指定两个参数。第一个参数代表数组中元素的类型,第二个代表数组的大小。
array<int, 3> arr = {9, 8, 7};
cout << "Array size = " << arr.size() << endl;
cout << "2nd element = " << arr[1] << endl;

       注意 C风格数组和std::arrays都有固定的大小,必须在编译阶段确定。它们无法在运行时扩大和缩小。

        如果想要动态大小的数组,建议使用下一节中介绍的std::vector。当添加一个元素时向量自动增加它的大小。
 
       std::vector
 
        C++标准库提供了一些大小不固定的容器用来存放消息。std::vector,声明在<vector>中,是这些容器中的一个例子。vector通过更加灵活和安全的机制替代了C风格数组的概念。作为用户不用担心内在管理,因为vector自动分配足够的内在来存放元素。vector是动态的,意味着在运行时添加和删除元素。17章深入容器的细节,但是vector的基本用法很直白,这就是在本书开头引入它的原因,这样可以在例子中使用它了。下面的代码展示了vector的基本功能。
// Create a vector of integers
vector<int> myVector = { 11, 22 };
// Add some more integers to the vector using push_back()
myVector.push_back(33);
myVector.push_back(44);
// Access elements
cout << "1st element: " << myVector[0] << endl;

         myVector被声明为vector<int>。正如std::array一样,需要用尖括号来指定模板参数。vector是通用容器,它几乎可以盛装任何类型;这就是需要在vector的尖括号里指定期望保存数据类型的原因。12章和22章讨论模板细节。可以使用push_back()方法向vector添加元素。可以使用类似数组的语法来访问每个元素,例如[]操作符。

        结构化绑定(Structured Bindings
 
        C++17引入了结构化绑定的概念。结构化绑定可以用数组、结构体、对(pair),元组的元素来初始化声明的多个变量。例如,假设有如下数组:
std::array<int, 3> values = { 11, 22, 33 };

        声明三个变量x、y、 z,如下用数组的三个值来初始化它们。注意必须使用关键字auto来进行结构化绑定。比如不能使用int来替代auto。

auto [x, y, z] = values;

        声明的结构化绑定变量的数量必须要和表达式右边值的数量匹配。如果结构体的所有非静态成员都是公有属性,那么也可以使用结构体来结构化绑定。例如,

struct Point { double mX, mY, mZ; };
Point point;
point.mX = 1.0; point.mY = 2.0; point.mZ = 3.0;
auto [x, y, z] = point;

       在第17章和20章会分别给出使用std::pair和std::tuple的例子。

        循环
 
        计算机最擅长一遍又一遍的重复同样的事情。C++提供了四种循环机制:while循环,do/while循环,for循环,基于范围的for循环。
 
        while循环
 
        while循环可以使一个代码块重复执行只要表达式的值为真。例如下面这段非常尬的代码将输出“This is silly.”五遍:
int i = 0;
while (i < 5) {
    std::cout << "This is silly." << std::endl;
    ++i;
}

        关键字break可以在循环中用来立即跳出循环并继续执行程序。关键字continue可以用来返回到循环的顶端,并重新评估while表达式的值。然而在循环中使用continue被认为是一种不好的风格,因为它会使程序的执行随意的跳转到某个地方,所以要保守的使用它。

        do/while循环
 
        C++也有一个while循环的变种版本叫做do/while。除了首先执行代码并在最后检测条件判断是否需要继续执行外,它的工作原理跟while循环一样。通过这种方式,do/while用在一个代码块必须执行至少一次,并根据某种条件决定额外的重复次数的情况中。下面的例子输出语句“This is silly.” 一次,即便条件最终为假:
int i = 100;
do {
    std::cout << "This is silly." << std::endl;
    ++i;
} while (i < 5);

        for循环

        for循环为循环提供了另一种语法。任何for循环都可以转换为while循环,反之亦然。然而,for循环的语法更加便捷,因为它以开始表达式、结束条件和在每个循环结束时执行的语句来看待循环的。下面的代码中i被初始化为0;只要i小于5,循环就继续执行;在每一次迭代结束,i自加1。这段代码和while循环的例子做同样的事情,但更有可读性,因为在一行中可同时看到开始值,结束条件,每次迭代语句。
for (int i = 0; i < 5; ++i) {
    std::cout << "This is silly." << std::endl;
}

        基于范围的for循环

       基于范围的for循环是第四种循环机制。它允许对一个容器的元素进行迭代。能用这种类型的循环的容器有C风格的数组,迭代列表(本章稍后讨论),任何有返回迭代器函数begin()和end()的类型(参见17章),例如std::array,std::vector和17章讨论的其它所有标准库容器。
       下面的代码首先定义了一个有四个整型的数组。然后基于范围的for循环在这个数组的每个元素的拷贝上迭代并输出每一个值。为了避免拷贝而在每一个元素本身上迭代,可以使用引用变量,正如本章后面讨论的。
std::array<int, 4> arr = {1, 2, 3, 4};
for (int i : arr) {
    std::cout << i << std::endl;
}
        初始化器列表
 
        初始化器列表定义在<initializer_list>头文件中,它使得书写接受可变参数的函数变得简单。initializer_list类是模板,因此需要在尖括号里面指定列表元素的类型,这跟指定存储在vector中元素的类型相似。下面的例子展示了如何使用初始化器列表:
#include <initializer_list>
using namespace std;
int makeSum(initializer_list<int> lst)
{
    int total = 0;
    for (int value : lst) {
        total += value;
    }
    return total;
}

        函数makeSum()用一个整型初始化器列表作为参数。函数体利用基于范围的for循环来累加总和。这个函数可以像下面这样使用:

int a = makeSum({1,2,3});
int b = makeSum({10,20,30,40,50,60});

        初始化器列表是类型安全的并且定义何种类型是列表可以接收的。对于这里展示的makeSum()函数,初始化器列表的所有元素必须是整型的。尝试用一个double来调用会导致编译错误或者警告,如下所示:

int c = makeSum({1,2,3.0});
        这些是基础知识(Those Are the Basics)
 
        此刻,你已经回顾了C++编程的基本要点。如果这一部分读起来很轻松,则略读下一部分以便对更高级知识的快速理解。如果感觉吃力,在开始继续前,你需要获得一本附录B中提到的优秀C++入门书。
 
        深入C++ 
 
        循环,变量和条件都是非常好的构建块,但是还有很多东西要学习。接下来的话题包括很多用来帮助C++程序员构建代码的特性,也包括一些困惑大于使用的特性。如果你是一个有一点C++经验的C程序员,则需要认真阅读本节。
 
    C++中的字符串
 
        在C++中有三种方式处理文本字符串:C风格,用字符数组表示字符串;C++风格,把这种表示封装成更易使用的字符串类型;非标准方法的通过类。第二章详细讨论。现在,仅需知道C++字符串类型定义在<string>头文件串,和可以像基本类型一样使用C++字符串。正如I/O流一样,字符串类型在std命名空间中。下面的例子展示了字符串可以像字符数组一样使用:
string myString = "Hello, World";
cout << "The value of myString is " << myString << endl;
cout << "The second letter is " << myString[1] << endl;

        指针和动态内存

        动态内存可以使程序的构建在编译阶段不固定数据的大小。很多很重要的程序都以某种方式来使用动态内存。
 
        栈和堆
 
        C++应用的内存分为两个部分——栈和堆。一种可视化栈的方式是将栈看作一幅扑克。当前顶卡代表程序的当前作用域,通常是正在被执行的函数。当前函数声明的所有变量都会在顶部栈帧,一幅牌顶上的卡片,占用内存。如果当前函数,我称为foo(),调用另一个函数bar(),一张新的卡片就被放到这幅牌上,这样bar()就有自己的栈来工作。任何由foo()传递给bar()的参数会从foo()的栈帧中拷贝到bar()的栈帧中。图1-2展示了虚构函数foo()执行期间栈可能的样子,它声明了两个整型值。栈桢很棒,它为每个函数提供独立的内存工作区。如果在foo()的栈中声明了一个变量,调用bar()时不会改变它,除非明确的告诉它要这么做。而且,当foo()函数运行结束时,栈帧被回收,函数内声明的所有变量也不再占用内存。由栈分配的变量不需要程序员来释放(删除),它是自动完成的。
 
                                                              
 
 
        堆是一块内存,它完全独立于当前函数或者栈帧。如果想在函数结束的时候变量还存在,就可以把它放在堆中。堆的结构性比栈弱。可以把它想像成一堆比特。程序可以在任何时候向堆中新增比特,也可以修改已存在的比特。必须确保在堆中已分配的内存被释放了。除了使用在“智能指针”一节讨论的智能指针外,释放内存不会自动完成。
 
        指针的使用( Working with Pointers
 
        通过显示的内存分配,可以把任何事物放在堆上。例如,为了把整型放在堆中,需要给它分配内存,但首先需要声明一个指针:
int* myIntegerPointer;

       int类型后面的*表明声明的变量引用或指向一些整型内存。可以将指针视为指向动态分配的堆内存的箭头。它还没有指向任何特定的东西因为还没有把它赋给任何东西;它是一个未初始化变量。任何时候都应该避开未初始化变量,特别是未初始化的指针,因为它们指向内存中某个随机的位置。使用这类指针很大概率导致程序挂死。这就是为什么应该总是在指针声明的同时初始化。如果不想立即分配内存可以把指针初始化为null指针(nullptr——参见“NULL指针常量”一节获得更多信息):

int* myIntegerPointer = nullptr;

       空指针是一个特殊的默认值,任何有效的指针都不会有这个值,并且在布尔表达式中可以转换为false。例如:

if (!myIntegerPointer) { /* myIntegerPointer is a null pointer */ }

        使用new运算符分配内存:

myIntegerPointer = new int;

        像这种情况,指针指向仅有一个整型值的地址。为了访问这个值,需要解引用指针。可以把解引用想像成顺着指针箭头找到位于堆上的实际值。为了给刚才在堆上分配的整型赋值,使用像下面这样的代码:

*myIntegerPointer = 8;

        注意,这与把myIntegerPointer 设置成8不同。没有改变指针,只是改变了指针指向的内存。如果重新分配指针的值,将它指向内存地址8,这个位置很可能是一个随机的无效值,最终会导致程序崩溃。使用完动态分配的内存后,需要使用delete运算符来释放内存。为避免使用已释放的指针,推荐将(已释放的)指针置为nullptr:

delete myIntegerPointer;
myIntegerPointer = nullptr;

        警告 有效的指针才能解引用。对null指针或未初始化的指针解引用导致未定义的行为。程序可能崩溃,也可能继续运行并产生异常结果。

        指针并不总是指向堆内存。也可以定义一个指向栈变量的指针,甚至是另一个指针。使用&(“谁的地址”)运算符来获取变量的指针:
 
int i = 8;
int* myIntegerPointer = &i; // Points to the variable with the value 8

        C++有处理结构体指针的特殊语法。在技术层面上,如果有一个指向结构体的指针,可首先使用*进行解引用,然后使用正常的.语法来访问它的域,如下代码所示,假定存在一个叫getEmployee()的函数。

Employee* anEmployee = getEmployee();
cout << (*anEmployee).salary << endl;

        这个语法有点啰嗦。->(箭头)运算符可以一步同时完成解引用和域访问。下面的代码与前面的等价,但更易读:

Employee* anEmployee = getEmployee();
cout << anEmployee->salary << endl;

        还记得本章稍早时讨论的短路逻辑的概念么?它跟指针组合以避免使用无效的指针非常有用。如下例子所示:

bool isValidSalary = (anEmployee && anEmployee->salary > 0);

        或稍详细点:

bool isValidSalary = (anEmployee != nullptr && anEmployee->salary > 0);

        只有anEmployee是有效指针时才解引用来获取salary。如果它是null指针,短路逻辑运算使得anEmployee不被解引用。

        动态分配数组
 
        堆也可以用来动态地分配数组。使用new[]运算符来给数组分配内存。
int arraySize = 8;
int* myVariableSizedArray = new int[arraySize];

        这为arraySize 个整数分配足够的内存。图1-3展示了这段代码执行后堆和栈的样子。正如你所见的,指针变量依然驻留在栈上,但是动态分配的数组在堆上。现在内存已经分配好了,可以像使用常规的栈数组那样使用它。

myVariableSizedArray[3] = 2;

 

        当程序使用完数组,它应该把数组从堆上删除以便其它变量可以使用这块内存。在C++中使用delete[]运算符来完成这个事情。
delete[] myVariableSizedArray;
myVariableSizedArray = nullptr;

        delete后面的括号指示正在删除一个数组!

        注意 避免使用C中的malloc()和free()。相反使用new和delete,或者new[]和delete[]。
        警告 为了避免内存泄漏,new和delete,new[]和delete[]应当成对使用。不调用delete或者delete[],或是调用不成对导致内存泄漏。在第7章讨论内存泄漏。
 
       Null指针常量
 
       在C++11之前,常量NULL常用于空指针。NULL简单的被定义为0,这会造成一些问题。看下面的例子:
void func(char* str) {cout << "char* version" << endl;}
void func(int i) {cout << "int version" << endl;}
int main()
{
    func(NULL);
    return 0;
}

        main()函数使用NULL参数来调用func函数。NULL被认为是空指针常量。也就是说你希望用空指针作为参数来调用char*版本的func()。然而,由于NULL不是指针,而是整数0,所以整数版本的func()被调用。引入真正的空指针常量nullptr可以解决这个问题。下面的代码调用char*版本的func():

func(nullptr);

        智能指针

        为了避免常见内存总是,应该使用智能指针取代“raw”指针,“raw”指针也被称作“naked”指针,C风格指针。当智能指针跳出做用范围时(例如,当函数完成退出时)会自动释放内存。下面是C++中两类最重要的智能指针,它们都定义在<memory>中,并位于std命名空间内:
        ➤ std::unique_ptr
        ➤ std::shared_ptr
        Unique_ptr类似于普通指针,但当Unique_ptr超出作用域时它会自动释放内存或者资源。unique_ptr拥有所指向对象的唯一所有权。unique_ptr的一个优点是:即使当return语句执行或者抛出异常时内存和资源总是会被释放。例如,当一个函数有多个返回语句时它可以简化编程,因为不用记住在每一个返回语句之前释放资源。可使用 std::make_unique<>()创建一个unique_ptr。比如,为替代下面的写法,
 
Employee* anEmployee = new Employee;
// ...
delete anEmployee;

         应该这样写:

auto anEmployee = make_unique<Employee>();

        注意不再需要调用delete了;它会自动调用的。在本章后面的“类型推断”一节会详细讨论auto关键词。现在知道auto关键词告诉编译器自动推导变量的类型就足够了,这样可以不用手动输入完整的类型了。unique_pt是一个通用智能指针,它可以指向任何类型的内存。这就是它是一个模板的原因。模板需要尖括号<>来指定模板参数。在括号内需要指定期望unique_ptr指向的内存类型。第12和22章讨论模板,但是智能指针在第1章介绍,这样在整本书中就可以使用它们了——你也将看到他们的用法很简单。

        从C++14才开始支持make_unique()。如果编译器不支持C++14则可以按如下方式创建unique_ptr(注意现在需要两次指定类型Employee):

unique_ptr<Employee> anEmployee(new Employee);
       可以像普通指针一样使用 anEmployee 智能指针,例如:
if (anEmployee) {
    cout << "Salary: " << anEmployee->salary << endl;
}

        unique_ptr可以用来保存C风格的数组。下面的例子创建一个具有10个Employee实例的数组,保存在unique_ptr中;也展示了如何从数组中访问元素的:

auto employees = make_unique<Employee[]>(10);
cout << "Salary: " << employees[0].salary << endl;

        shared_ptr允许共享数据所有权。每次shared_ptr 被赋值一次,引用计数就是自增1,代表数据又多了一个所有者。当一个shared_ptr超出作用域时,引用计数就自减1。当引用计数减为0时意味着数据已没有所有者了,指针所指向的对象被释放掉。应当使用std::make_shared<>()创建shared_ptr,与make_unique<>()类似:

auto anEmployee = make_shared<Employee>();
if (anEmployee) {
    cout << "Salary: " << anEmployee->salary << endl;
}

        从C++17开始,shared_ptr也可用来保存数组,老的C++是不允许这样的。但注意C++17中make_shared<>() 不能用于这种情况。这里有个例子:

shared_ptr<Employee[]> employees(new Employee[10]);
cout << "Salary: " << employees[0].salary << endl;

        第7章更详细的讨论内存管理和智能指针,但是因为unique_ptr和shared_ptr的基本用法非常直白,所以它们已在全书的例子中使用。

        注意 当不涉及所有权是裸指针才被允许使用。否则,默认使用unique_ptr,需要共享所有权时使用shared_ptr。如果你了解auto_ptr,忘掉它;它 在c++ 11/14中已弃用,并已从c++ 17中移除。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值