事项说明:

  1. 以前为了过c++程序设计课,学过《c++ primer plus》,此书就是c++入门的圣经。如今只是想要重拾c++,这种大部头虽然可以再看一遍,但是未免过于费时,所以挑选了较为小巧的《c++入门经典》,这两本书在我心中都有不错的印象,但是想要快速复习一下基础内容,则是十分推荐《入门经典》
  2. 本系列只是《c++入门经典》中各章的随笔记录与代码抄录,可能有时会附上个人的注解
  3. 使用的编辑器为vs code,编译器为gcc9.2.0,选别的肯定也行,但我就喜欢这样

第一章 编写第一个程序

1.1使用c++

介绍而已,没啥好说的,略

1.2编译和链接源代码

对于您创建的c++源代码文件,可使用扩展名.cpp、.cxx、.cp或.c。大多数c++都不关心源代码文件的扩展名,但使用.cpp有助于您识别源代码。

编译源代码时,将生成一个目标文件,链接器将把它转换为可执行程序

创建c++程序时,将链接一个或多个目标文件以及一个或多个库。库是一系列可链接的文件,提供了有用的函数和类,可供在程序中使用。函数是一个执行任务的代码块。类定义了一种新数据类型和相关的函数。

创建c++程序的步骤如下:

  1. 使用文本编辑器创建源代码。

  2. 使用编译器将源代码转换为目标文件。

  3. 使用链接器链接目标文件和必要的库,生成可执行程序。

  4. 输入可执行文件的名称以运行它。

    gcc编译器将编译和链接合而为一。

1.3创建第一个程序

程序清单1.1 Motto.cpp

#include<iostream>
int main()
{
    std::cout<<"Solidum petit in profundis!\n";
    return 0;
}

命令台编译命令:

(windows下)

g++ Motto.cpp -o Motto.exe

即:g++ 源程序名 -o 可执行程序名(后缀根据操作系统平台可能会产生差异)

其余部分

不重要,略。

第二章 程序的组成部分

2.1使用c++的原因

组成计算机程序的指令称为源代码

基于解释器的语言每次读取一行代码,并将指令进行转换

基于编译器的语言通过编译将程序转换为目标代码,这些代码存储在目标文件中。然后,由链接器将目标文件转换为可在操作系统上运行的可执行程序。

c++特点:快!

面向过程:程序被设计为一系列操作,这些操作对一组数据进行处理。

​ 结构化编程主要思想:分而治之

面向对象:将数据和操作数据的过程视为一个对象:一个有身份和特征(即行为和方法)的独立实体。

c++全面支持面向对象编程(笑了,c++这个体量还有什么不能支持),包括面向对象开发的三个支柱概念:封装、继承和多态。

2.2程序的组成部分

对于第一章中的示例代码:

程序清单2.1 Motto.cpp

#include<iostream>
int main()
{
    std::cout<<"Solidum petit in profundis!\n";
    return 0;
}

第一行,#指出这一行是一个将由预处理器处理的命令,编译指令#include告诉预处理器,将指定文件的全部内容加到指定位置。文件名iostream前后的<>告诉预处理器,前往一组标准位置(也就是标准函数库)寻找该文件(也可使用#include"iostream",但是这样是告诉预处理器先从当前目录寻找该头文件,找不到再到标准位置寻找)

也就是在第一行,将插入找到的iostream(实际上文件全名为iostream.h)的全部内容,为标准输入输出流头文件。

第二行,则是main()函数,每个c++程序(此处是程序而不是文件)都包含一个main()函数,程序运行时自动调用main()函数。int为函数返回类型,()为参数列表,{}为函数体。

函数体内,第一行则使用cout命令进行消息输出,而std::则是指定名称空间对其进行限定,告诉编译器,此处使用标准输入输出库。<<为输出重定向符,后接字符串"Solidum petit in profundis!\n"\n为换行符,不多说。第二行为对应之前指出的函数返回值int,此处则返回0,当然,此处的返回值0自然是返回给操作系统的,通常main()返回值为0表示程序运行成功,返回其他数字则是表示出现了某种故障。

2.3注释

//这是单行注释
/*
这是多行注释第一行
这是多行注释第二行
*/
这是没有注释会报错

2.4函数

即:

返回值类型 函数名 (形式参数列表)

{函数体}

例如:

int add(int x,int y){
    //add this numbers x and y together and return the sum
    std::cout<<"Running  Caculator ...\n";
    return (x + y);
}

程序清单2.2 Caculator.cpp

#include<iostream>

int add(int x,int y)
{
    //add this numbers x and y together and return the sum
    std::cout<<"Running  Caculator ...\n";
    return (x + y);
}

int main()
{
    std::cout<<"what is 867 +5309?\n";
    std::cout<<"the sum is: "<<add(867,5309)<<"\n\n";
    std::cout<<"what is 777 +9311?\n";
    std::cout<<"the sum is: "<<add(777,9311)<<"\n";
    return 0;
}

实参与形参

实参是传递给函数的信息(你可以理解为传递过去的数据),形参是函数收到的信息。调用函数时,提供的是实参,而函数内部,收到的实参值存储在形参中。

第三章 创建变量和常量

3.1变量是什么

变量是计算机内存中的一个位置,您可以在这里存储和检索值。变量有地址,并赋予了描述其用途的名称。

例如int zombies = 2;其中zombies是变量名,2是其存储的变量值,但是它可能被放在了内存中的第101~104块,那么它的地址就是101~104(实际可能只显示头部地址)

c++创建变量时,须指定变量名称和类型,比如int a;,数据类型用于指定变量所占用的最大空间(单位为字节)

基本数据类型:(因操作系统平台不同,长度可能会有所不同)

类型长度(字节数)取值范围
unsigned short20~65 535
short2-32 768~32 767
unsigned long40~4 294 967 295
long4-2 147 483 648~2 147 483 647
int4-2 147 483 648~2 147 483 647
unsigned int40~4 294 967 295
long long int8-9.2x101 ~9.2x101
char1256个字符
bool1true或false
float41.2e-38~3.4e38
double82.2e-308~1.8e308

其中须注意的是无符号数的长度虽然与有符号数的相同,但是从0开始

最特殊的是浮点数,浮点数的制定与其他的不同,它是依据IEEE754标准制定的,浮点表示法应该是一个programer掌握的最基本知识(以后会有深入理解计算机系统笔记,到时候会再行说明)

当然,c++也支持用户自定义的变量类型,比如用户定义的类或结构体。

要获取变量类型的长度,可使用sizeof()函数

程序清单3.1 Size.cpp

#include<iostream>
int main(){
    std::cout<<"The size of an integer:\t\t";
    std::cout<<sizeof(int)<<" bytes\n";
    std::cout<<"The size of a short integer:\t";
    std::cout<<sizeof(short)<<" bytes\n";
    std::cout<<"The size of a long integer:\t";
    std::cout<<sizeof(long)<<" bytes\n";
    std::cout<<"The size of a character:\t";
    std::cout<<sizeof(char)<<" bytes\n";
    std::cout<<"The size of a boolean:\t\t";
    std::cout<<sizeof(bool)<<" bytes\n";
    std::cout<<"The size of a float:\t\t";
    std::cout<<sizeof(float)<<" bytes\n";
    std::cout<<"The size of a double float:\t\t";
    std::cout<<sizeof(double)<<" bytes\n";
    std::cout<<"The size of a long long int:\t\t";
    std::cout<<sizeof(long long int)<<" bytes\n";
}

3.2定义变量

驼峰命名法:

对变量命名使用小驼峰命名法(首单词小写,其余单词首字母大写),如:newWroldRecord

对类名,名称空间使用大驼峰命名法(单词首字母均大写),如:DataBaseManager

除了驼峰命名法之外,没啥可记录的了。

3.3给变量赋值

赋值运算符: =

程序清单3.2 Rectangle.cpp

#include <iostream>

int main()
{
    unsigned short width = 26, length;
    length = 40;
    unsigned short area = width * length;
    std::cout << "Width: " << width << "\n";
    std::cout << "Length: " << length << "\n";
    std::cout << "Area: " << area << "\n";
    return 0;
}

3.4类型定义

类型定义关键字:typedef

程序清单3.3 NewRectangle.cpp

#include <iostream>

int main()
{
    typedef unsigned short USHORT;
    USHORT width = 26;
    USHORT length = 40;
    USHORT area = width * length;
    std::cout << "Width: " << width << "\n";
    std::cout << "Length: " << length << "\n";
    std::cout << "Area: " << area << "\n";
    return 0;
}

3.5常量

常量值不会改变,且必须在创建常量时对其进行初始化。c++支持两种类型的常量:字面常量与符号常量

字面常量是直接在需要的地方输入的值

比如:long width = 19;中,19就是字面常量,true与false也是字面常量。

符号常量是用名称表示的常量,与变量类型相似。声明符号常量时,需要使用关键字const,并在后面跟类型、名称和初值。

比如:const int KILL_BONUS = 5000;

也可定义常量,如:#define KILLBONUS 5000,由于这种c式定义法没有指定类型,编译器无法确保其值是合适的,所以建议采用const关键字,而非使用编译指令进行常量定义。

枚举常量是在一条语句中创建一组常量,使用关键字enum定义,枚举值放在大括号里,之间逗号进行分隔。

比如:enmu Color {RED=100,BLUE,GREEN=500,WHITE,BLACK=700};

程序清单3.4 Compass.cpp

#include <iostream>

int main()
{
    enum Direction
    {
        North,
        Northeast,
        East,
        Southeast,
        South,
        Southwest,
        West,
        Northwest
    };
    Direction heading;
    heading = Southeast;
    std::cout << "Moving " << heading << std::endl;
    return 0;
}

这里的输出结果是moving 3,原因是如果不进行显式初始化枚举常量,则枚举值从North开始默认为0,然后递增。

3.6自动变量

c++有一个关键字auto,可用于根据赋给变量的初值推断出变量的类型,该工作由编译器完成。

比如:auto rate=500/3.0等价于double rate=500/3.0

程序清单3.5 Combat.cpp

#include <iostream>

int main()
{
    auto strength = 80;
    auto accuracy = 45.5;
    auto dexterity = 24.0;

    const auto MAXIMUM = 50;

    auto attack = strength * (accuracy/MAXIMUM);
    auto damage = strength * (dexterity/MAXIMUM);
    std::cout<<"\nAttack rating: "<<attack<<"\n";
    std::cout<<"Damage rating: "<<damage<<"\n";

    return 0;
}

结果是:

Attack rating: 72.8 Damage rating: 38.4

如果上面这个程序您的编译器发出报错或警告,可能是您的编译器默认编译的c++版本在14以下,则使用以下编译命令尝试让编译器选择使用c++14进行编译:

g++ -std=c++14 Combat.cpp -o Combat.exe

(Linux下大同小异)

第四章 使用表达式、语句和运算符

4.1语句

所有的c++都由语句组成,语句是以分号结尾的命令。语句控制程序的执行流程、评估表达式甚至可以什么也不做(空语句)。

空白:

在c++程序源代码中,空格、制表符和换行符统称为空白。空白旨在让程序员方便阅读代码,编译器通常忽略他们。

正确的缩进有助于识别程序块或函数块的开始和结束位置。

复合语句:

可将多条语句编组,构成一条复合语句,这种语句以{开头,以}结束。可将复合语句放在任何可使用单条语句的地方。

复合语句中的每条语句都必须以分号结尾,但复合语句本身不能以分号结尾。如:

{
    temp = a;
    a = b;
    b = temp;
}

这条复合语句交换a与b的值,交换时使用变量临时存储了一个变量的值。

4.2表达式

表达式是语句中任何返回一个值的部分。如:

z = x = y + 13;

这条语句包含三个表达式:

  • 表达式 y + 13,值被存储在变量x中;

  • 表达式x = y + 13,它返回变量x的值,而该返回值被存储在变量z中;

  • 表达式z = x = y + 13,它返回z的值,但是该返回值并未存储到其他变量中。

    赋值运算符=导致左操作数的值变为右操作数的值

    操作数是一个数学术语,指的是被运算符操作的表达式。

程序清单4.1 Expression.cpp

#include <iostream>

int main()
{
    int x = 12, y = 42, z = 88;
    std::cout << "Before -- x: " << x << " y: " << y;
    std::cout << " z: " << z << "\n\n";
    z = x = y + 13;
    std::cout<<"After -- x: "<<x<<" y: "<<y;
    std::cout<<" z: "<<z<<"\n";
    return 0;
}

4.3运算符

运算符是导致编译器执行操作的符号。

  • 赋值运算符:=(常量可以作为右值,但不能作为左值,比如95 = grade非法)

  • 数学运算符:+、-、*、/、%(%为求模运算符,返回整数除法的余数)

  • 组合运算符:+=、-=、*=、/=、%=(都是自赋值运算符)

  • 关系运算符:==、!=、>、>=、<、<=

  • 逻辑运算符:&&、||、!

  • 位运算符:&、|、^、~、<<、>>

  • 递增与递减运算符:++、--

  • 前缀运算符与后缀运算符:递增运算符++与递减运算符--若放在变量前面,就称为前缀运算符,放在变量后面则称为后缀运算符。

    如:++count;中的++为前缀运算符,count++;中的++为后缀运算符

    区别在于进行赋值时,前缀运算符会在赋值前执行,而后缀运算符会在赋值后执行

    int x = 5;
    int sum = ++x;
    

    这两条语句使得x为6;sum为6;

    int x = 5;
    int sum = x++;
    

    这两条语句导致sum为5,x为6。

程序清单4.2 Years.cpp

#include <iostream>

int main()
{
    int year = 2016;
    std::cout<<"The year "<<++year<<" passes.\n";
    std::cout<<"The year "<<++year<<" passes.\n";
    std::cout<<"The year "<<++year<<" passes.\n";

    std::cout<<"\nIt is now"<<year<<".";
    std::cout<<" Have the Chicago Cubs won the World Series yet?\n";
    std::cout<<"\nThe year "<<year++<<" passes.\n";
    std::cout<<"The year "<<year++<<" passes.\n";
    std::cout<<"The year "<<year++<<" passes.\n";

    std::cout<<"\nSurely the Cubs have won the Series by now.\n";
    return 0;
}

运算符优先级:表就不打了,自己去查,貌似版本挺多。

4.4if-else条件语句

程序清单4.3 Grader.cpp

#include <iostream>

int main()
{
    int grade;
    std::cout << "Enter a grade (1-100): ";
    std::cin >> grade;

    if (grade >= 70)
        std::cout << "\nYou passed. Hooray!\n";
    else
        std::cout << "\nYou failed. sigh.\n";
    return 0;
}

程序清单4.4 NewGrader.cpp

#include <iostream>

int main()
{
    int grade;
    std::cout << "Enter a grade (1-100): ";
    std::cin >> grade;

    if (grade >= 70)
    {
        if (grade >= 90)
        {
            std::cout << "\nYou got an A. Great job!\n";
            return 0;
        }
        if (grade >= 80)
        {
            std::cout << "\nYou got a B. Good work!\n";
            return 0;
        }
        std::cout << "\nYou got a C.\n";
    }
    else if (grade >= 60)
    {
        std::cout << "\nYou got a D.\n";
    }
    else
        std::cout << "\nYou got an F. Congratulations!\n";
}

第五章 调用函数

5.1函数是什么

函数是程序的一部分,可对数据执行操作并返回一个值。每个c++至少有一个函数:main()

5.2声明与定义函数

编写函数代码前应该声明该函数,包括返回类型、函数名和形参列表

函数定义之前说过了,此处就略了。

例如:

int getArea(int length,int width);//函数声明

int getArea(int length,int width)
{
    return length * width;
}//函数定义

5.3在函数中使用变量

局部变量与全局变量:

局部变量:函数内创建的变量为局部变量,函数返回后,其所有局部变量都不能供程序使用。

全局变量:在c++中,可在函数(包括main()函数)外定义c++变量,这种全局变量在程序的任何地方都可用。函数外定义的变量的作用域为全局,音词可在程序的任何函数中使用

我认为,只要是被代码块{}所包裹起来的变量都是局部变量,而在代码块之外定义的变量则是全局变量

程序清单5.3 Global.cpp

#include <iostream>
void convert();

float fahrenheit;
float celsius;

int main()
{
    std::cout<<"Please enter the temperature in Fahrenheit: ";
    std::cin>>fahrenheit;
    convert();
    std::cout<<"\nHere's the temperature in Celsius: ";
    std::cout<<celsius<<"\n";
    return 0;
}

void convert()
{
    celsius = ((fahrenheit - 32) * 5) / 9;
}

5.4函数形参

5.5从函数返回值

函数返回一个值或void。要从函数返回一个值,可使用关键字return,并在它后面指定要返回的值。这个值可以是字面量、变量或表达式,因为所有表达式都生成一个值。

程序清单5.4 LeapYear.cpp

#include <iostream>
bool isLeapYear(int year);

int main()
{
    int input;
    std::cout << "Please enter a year: ";
    std::cin >> input;
    if (isLeapYear(input))
        std::cout << input << " is a leap year\n";
    else
        std::cout << input << " is not a leap year\n";
    return 0;
}

bool isLeapYear(int year)
{
    if (year % 4 == 0)
    {
        if (year % 100 == 0)
        {
            if (year % 400 == 0)
                return true;
            else
                return false;
        }
        else
            return true;
    }
    else
        return false;
}

5.6默认函数形参

程序清单5.5 AreaCube.cpp

#include <iostream>
int findArea(int length,int width=20,int height = 12);

int main()
{
    int length = 100;
    int width =50;
    int height = 2;
    int area;

    area=findArea(length,width,height);
    std::cout<<"First area: "<<area<<"\n\n";
    area=findArea(length,width);
    std::cout<<"second area: "<<area<<"\n\n";
    area=findArea(length);
    std::cout<<"Third area: "<<area<<"\n\n";
    return 0;
}

int findArea(int length,int width,int height)
{
    return (length * width * height);
}

5.7函数重载

c++中,可以有多个同名函数,只要他们的形参不同即可,这称为函数重载,又称函数多态

多个重载版本的返回值可以相同,也可以不同,然而,不能通过修改返回类型来重载函数,相反,形参类型与形参数量必须不同

举例:

int average(int,int);
long average(long,long);
float average(float,float);//函数定义就不写了

调用函数average()时,只需传递合适的数据,就将调用相应的重载版本。

内联函数:

当定义函数时,c++编译器只在内存中创建一组指令,每当调用该函数时,都将跳转到这些指令,而函数返回时,将跳转到调用代码的下一行。如果程序调用了函数10次,每次都将跳转到同一组指令,即只有一个函数指令拷贝,而不是10个。跳转到函数和返回有一定的开销,如果函数包含的语句很少,就可以通过避免跳转来提高效率。在这种情况下,通过避免函数调用,程序的运行速度将更快。

声明c++函数时,如果使用了关键字inline,编译器将不会创建该函数,而将代码直接复制到调用它的地方,就像您在哪里输入了函数的语句一样。如果该内联函数被调用和10次,内联代码将总共复制10次,细微的速度改善可能因可执行程序的增大而抵消。

要将函数声明为内联的,可在函数原型中使用关键字inline。

inline int double(int);

而不用修改函数本身:

int double(int target)
{
    return 2 * target;
}

5.8自动确定返回值类型

c++14新增的功能之一是,使用关键字auto让编译器自动推断函数的返回类型

程序清单5.6 AutoCube.cpp

#include <iostream>
auto findArea(int length,int width=20,int height = 12);\

auto findArea(int length,int width,int height)
{
    return (length * width * height);
}

int main()
{
    int length = 100;
    int width =50;
    int height = 2;
    int area;

    area=findArea(length,width,height);
    std::cout<<"First area: "<<area<<"\n\n";
    area=findArea(length,width);
    std::cout<<"second area: "<<area<<"\n\n";
    area=findArea(length);
    std::cout<<"Third area: "<<area<<"\n\n";
    return 0;
}

第六章 控制程序流程

6.1 循环

在程序中执行多次的代码块称为循环,其中每次循环都称为迭代

6.2 while循环

while循环导致程序重复执行一组语句,直到开始条件为false。

下面的while循环显示数字0~99:

int x = 0;
while(x < 100)
{
    std::cout<< x <<"\n";
    x++;
}

程序清单6.1中,程序Thirteens使用一个while循环显示可被13整除且小于500的所有数字。

程序清单6.1 Thirteens.cpp

#include <iostream>
int main()
{
    int counter = 0;
    while (counter < 500)
    {
        counter++;
        if (counter % 13 == 0)
        {
            std::cout << counter << " ";
        }
    }
    std::cout << "\n";
    return 0;
}

break语句导致循环立即终止,而不等待条件为false。

程序清单6.2这个程序显示前20个可被14整除的数

程序清单6.2 Fourteens.cpp

#include <iostream>
int main()
{
    int counter = 0;
    int multiples = 0;
    while (true)
    {
        counter++;
        if (counter % 14 == 0)
        {
            std::cout << counter << " ";
            multiples++;
        }
        if (multiples > 19)
        {
            break;
        }
        }
    std::cout << "\n";
    return 0;
}

另一种改变循环行为的方式是使用continue语句。在循环中遇到continue语句时,将跳过余下的语句,开始下一次循环迭代

程序清单6.3这个程序显示前20个可被15整除的数

程序清单6.3 Fifteens.cpp

#include <iostream>
int main()
{
    int counter = 0;
    int multiples = 0;
    while (multiples < 19)
    {
        counter++;
        if (counter % 15 != 0)
            continue;
        std::cout << counter << " ";
        multiples++;
    }
    std::cout << "\n";
    return 0;
}

6.3 do-wihle循环

while循环执行循环语句前检查条件表达式,如果条件不可能为true,循环语句就不会执行。

而do-while语句将在循环末尾检查条件

也就是do-while循环的循环体至少会执行一次

程序清单6.4 Badger.cpp

#include <iostream>
int main()
{
    int badger;
    std::cout<<"How many badgers? ";
    std::cin>>badger;
    do
    {
        std::cout<<"Badger ";
        badger--;
    } while (badger > 0);
    std::cout<<"\n";
    return 0;
}

6.4 for循环

for循环是一种复杂的循环,将设置计数器变量、检查计数器变量是否满足条件、迭代修改计数器变量这三个步骤合并到了一条语句中。该语句使用关键字for,后面是一对括号。括号内,是三条用分号分隔的语句,它们分别初始化计数器、检查条件和修改计数器

程序清单6.5 MultTable.cpp

#include <iostream>
int main()
{
    int number;
    std::cout << "Enter a number:";
    std::cin >> number;

    std::cout << "\nFirst 10 Multiples of " << number << "\n";
    for (int counter = 1; counter < 11; counter++)
    {
        std::cout << number * counter << " ";
    }
    std::cout << "\n";
    return 0;
}

多变量for循环:

for(int x = 0,y = 0; x < 10;x++,y++)
{
    std::cout<< x * y << "\n";
}

这个就等同于:

int x = 0,y = 0;
for(; x < 10;x++,y++)
{
    std::cout<< x * y << "\n";
}

嵌套循环:

程序清单6.6 BoxMaker.cpp

#include <iostream>
int main()
{
    int rows, columns;
    char character;

    std::cout << "How many rows? ";
    std::cin >> rows;
    std::cout << "How many columns? ";
    std::cin >> columns;
    std::cout << "what character to display? ";
    std::cin >> character;

    std::cout << "\n";
    for (int i = 0; i < rows; i++)
    {
        for (int j = 0; j < columns; j++)
        {
            std::cout << character;
        }
        std::cout << "\n";
    }
    return 0;
}

6.5 switch语句

switch语句检查一个表达式,并根据其值执行多个代码块中的一个

std::cout<<"You have kulled "<< zombies << " zombie";
switch(zombies)
{
    case 0:
        std::cout << "s\n";
        break;
    case 1:
        std::cout << "\n";
        break;
    default:
        std::cout << "s\n";
}

在switch语句的case部分,只能进行相等比较,而不能进行关系运算和布尔运算。如果有case值与表达式匹配,将执行相应的语句,然后继续执行到switch块末尾或遇到的第一个break语句。如果没有匹配的case部分,将执行可选的default部分。

在上例中,每个case部分都以一条break语句结尾,用于退出switch语句。如果case部分末尾没有break语句,将继续执行下一个case部分。在有些情况下,可利用这种办法来只想多个case部分。

程序清单6.7 BadTeacher.cpp

#include <iostream>
int main()
{
    char grade;
    std::cout << "Enter your letter grade (ABCDF):";
    std::cin >> grade;
    switch (grade)
    {
    case 'A':
        std::cout << "Finally!\n";
        break;
    case 'B':
        std::cout << "You can do better!\n";
        break;
    case 'C':
        std::cout << "I'm disappointed in you!\n";
        break;
    case 'D':
        std::cout << "You are not smart!\n";
        break;
    case 'F':
        std::cout << "Get out of my sight!\n";
        break;
    default:
        std::cout << "That's not even a grade!\n";
        break;
    }
    return 0;
}

第七章 使用数组和字符串存储信息

7.1数组是什么

数组是一系列类型相同的相关数据。可将数组视为一系列数据存储单元,其中每个存储单元都是数组的一个元素。

要声明数组,可依次指定数据类型、数组名以及用方括号括起的元素数,如下所示:

long peaks[25];

数组元素从0开始编号,直到n-1(n为数组长度)

程序清单7.1 WeightGoals.cpp

#include <iostream>
int main()
{
    float goal[4];
    goal[0]=0.9;
    goal[1]=0.75;
    goal[2]=0.5;
    goal[3]=0.25;
    float weight,target;

    std::cout<<"Enter current weight: ";
    std::cin>>weight;
    std::cout<<"Enter goal weight: ";
    std::cin>>target;
    std::cout<<std::endl;

    for (int i = 0; i < 4; i++)
    {
        float loss = (weight - target) * goal[i];
        std::cout<<"Goal "<<i<<": ";
        std::cout<<target+loss<<std::endl;
    }
    return 0;
}

7.2写入时超过数组末尾

数组越界没啥好说的,略

7.3初始化数组

例如:

int post[10] = {0,10,20,30,40,50,60,70,80,90};

如果省略长度,它包含的元素数将等于初始值数。

int post[] = {0,10,20,30,40,50,60,70,80,90};//10

获取数组元素,最常用的使用sizeof()

const int size = sizeof(post) / sizeof(post[0]);

初始化的元素数量不能超过声明的长度,比如:

int array[3] = {1,2,3,4};//编译器报错
int arr[3] = {1,2};//允许

7.4多维数组

n维数组是n-1维数组的堆叠

比如:

int board[8][8];//二维数组

就是八个长度为8的一维数组的堆叠,所以也可以用以下方法存储这些数据:

int board[64];

初始化多维数组:

程序清单7.2 Box.cpp

#include <iostream>
int main()
{
    int box[5][3] = {
        {8, 6, 7},
        {5, 3, 0},
        {9, 2, 1},
        {7, 8, 9},
        {0, 5, 2}};
        for (int i = 0; i < 5; i++)
        {
            for (int j = 0; j < 3; j++)
            {
                std::cout<<"box["<<i<<"]";
                std::cout<<"["<<j<<"] = ";
                std::cout<<box[i][j]<<"\n";
            }
        }
    return 0;
}

也可各维元素放在一起,但是会比较难看:

    int box[5][3] = {8, 6, 7,5, 3, 0,9, 2, 1,7, 8, 9,0, 5, 2};

7.5字符数组

在c++中,字符串是以空字符结尾的字符数组,空字符是以编码为'\0'的特殊字符。可像其他数组那样声明并初始化字符串

char yum[] = {'Z','0','m','b','i','e',' ','E','a','t',' ','B','r','a','i','n','s','\0'};

最后的'\0'是用于结束字符串的空字符

这种逐个字符输入的方法比较困难,很容易出错,c++提供了使用字面量初始化字符串的简捷方式:

char yum[] = "Zombie Eat Brains";

这种初始化方法不需要使用空字符,而是由编译器自动添加的。

字符串"Zombie Eat Brains"占用18个字节(包括空字符)

也可创建未初始化的字符数组,这被称为缓冲区。

缓冲区可用于存储用户输入,虽然std::cin>>yum;可以将输入存储到变量中,但是如果用户输入的字符数超过了缓冲区的长度,cin写入时将跨过缓冲区边界,导致程序不能正确运行,更可能导致安全问题。其次,如果用户输入了空格,cin将认为字符串就此结束,不再将接下来的内容写入缓冲区

为解决这些问题,必须调用cin对象的getline()函数,并提供两个参数:

  • 要填充的缓冲区
  • 最多读取的字符数

下面的语句就是将用户输入中最多18个字符(包括空格)读取,将其存储到字符数组yum中:

std::cin.getline(yum,18);

调用这个方法时,还可提供第三个参数——终止输入的分隔符:

std::cin.getline(yum,18,' ');

这条语句在遇到空格之后停止读取输入。如果省略了第三个参数,则将换行符'\n'作为分隔符

程序清单7.3 BridgeKeeper.cpp

#include<iostream>
int main()
{
    char name[50];
    char quest[80];
    char velocity[80];

    std::cout<<"\nWhat is your name? ";
    std::cin.getline(name,49);
    std::cout<<"\nWhat is your quest? ";
    std::cin.getline(quest,79);
    std::cout<<"\nWhat is the velocity of an unladen swallow? ";
    std::cin.getline(velocity,79);
    
    std::cout<<"\nName:"<<name<<std::endl;
    std::cout<<"Quest:"<<quest<<std::endl;
    std::cout<<"Velocity:"<<velocity<<std::endl;
    return 0;   
}

运行如下:

这三个问题来自电影《巨蟒与圣杯》。

为什么在输入时输入的字符数比数组长度小1呢?这是因为需要预留(至少是最后那一个)位置给表示字符串终止的'\0'字符。

7.6复制字符串

string.h是专门用以处理字符串的函数库

头文件添加它的两种办法就是#include<cstring>或者#include<string.h>

这个库里面有两个用于将一个字符串复制到另一个字符串的函数,一个是strcpy(),另一个是strncpy()

strcpy与strncpy的区别:

strcpy是将整个数组复制到指定缓冲区(也就是另一个数组),但是这样可能会导致写入时超过缓冲区的边界。

strcpy_s(strings2,string1)//将整个s1复制到s2,strcpy_s是strcpy的安全版本函数,如果不用这个也可以,但有的编译器会警告或报错,比如vc++

strncpy接受第三个参数,指定最多复制多少个字符:

strncopy(string1,string2,80);

程序清单7.4 StringCopier.cpp

#include <iostream>
#include <cstring>

int main()
{
    char string1[] = "Free the bound periodicals!";
    char string2[80];
    char string3[20];

    strcpy(string2, string1);

    std::cout << "String1: " << string1 << std::endl;
    std::cout << "String2: " << string2 << std::endl;

    strncpy(string3,string1,19);
    std::cout << "String1: " << string1 << std::endl;
    std::cout << "String3: " << string3 << std::endl;
}

7.7使用foreach循环读取数组

c++新增了一种用于遍历数组元素的for循环,这种for循环通常被称为foreach循环,因为它对每个与那苏都执行循环一次

程序清单7.5 Production.cpp

#include <iostream>

int main()
{
    int production[]{10500, 16000, 5800, 4500, 13900};
    for (int year : production)
    {
        std::cout << "Output: " << year << std::endl;
    }
    return 0;
}

第八章 创建基本类

8.1类型是什么

8.2创建新类型

变量的类型提供了多项信息:

  • 变量占据的内存
  • 变量可存储的信息
  • 对变量可执行的操作

在c++中,可自己定义类型,以模拟要解决的问题。要声明新问题,可创建一个类。类是新类型的定义。

8.3类和成员

c++类是一个模板,用于创建对象。定义类后,便可像使用其他类型那样使用根据它创建的对象。

类是一系列捆绑在一起的变量和函数,其中的变量可以是任何其他类型,也包括其他类。

类中的变量称为成员变量,类中的函数称为成员函数或方法。

其实成员就是数据与操作,封装之后就成为了一个类。

声明一个类,使用关键字class,比如:

class Tricycle
{
public:
    unsigned int speed;
    unsigned int wheelSize;
    pedal();
    break();
};

创建一个对象,可指定类名和变量名,比如:

Tricycle wichita;

8.4访问类成员

创建对象后,可使用句点运算符(.)来访问其成员函数和成员变量。

比如承接上面的:

Tricycle wichita;
wichita.speed = 6;
wichita.peadl();

访问控制符public、protected、private就不说了,省得长篇大论。

8.5实现成员函数

对于声明的每个成员函数,都必须进行定义

成员函数的定义可以在类中,也可以在类外,在类外定义以类名打头,然后是作用域解析运算符(::)和函数名再加函数体。

比如:

void Tricycle::pedal()
{
    std::cout<<"Pedaling trike\n";
}

程序清单8.1Tricycle.cpp

#include <iostream>

class Tricycle
{
public:
    int getSpeed();
    void setSpeed(int speed);
    void pedal();
    void brake();

private:
    int speed;
};

int Tricycle::getSpeed()
{
    return speed;
}

void Tricycle::setSpeed(int newSpeed)
{
    if (newSpeed >= 0)
    {
        speed = newSpeed;
    }
}

void Tricycle::pedal()
{
    setSpeed(speed + 1);
    std::cout << "\nPedaling;tricycle speed " << speed << " mph\n";
}

void Tricycle::brake()
{
    setSpeed(speed - 1);
    std::cout << "\nBraking;tricycle speed " << speed << " mph\n";
}

int main()
{
    Tricycle wichita;
    wichita.setSpeed(0);
    wichita.pedal();
    wichita.pedal();
    wichita.brake();
    wichita.brake();
    wichita.brake();
    return 0;
}

8.6创建和删除对象

构造函数与析构函数:

类有一个特殊的成员函数——构造函数,每次实例化对象时都将调用它。构造函数的职责是创建一个有效的对象,这通常包括初始化成员数据。构造函数与类同名,且没有返回值。构造函数可以接受参数,也可以不接受。

Tricycle类的构造函数:

Tricycle::Tricycle(int initialSpeed)
{
    setSpeed(initialSpeed);
}//使用了参数initialSpeed来设置成员变量speed的初始值

如果声明了构造函数,也应声明一个析构函数。构造函数创建并初始化对象,而析构函数执行清理工作并释放分配给对象的内存。析构函数的名称总是有腭化符号(~)和类名组成。析构函数不接收参数,也不返回值。

Tricycle类的析构函数:

Tricycle::~Tricycle()
{
    std::cout<<"Destructed\n";
}

默认构造函数

假如您没有声明构造函数,编译器将自动创建一个默认构造函数——没有参数的构造函数,编译器创建的这个默认构造函数不执行任何操作,就如同没有参数且函数体为空一样。

  • 默认构造函数是没有参数的构造函数,可以自己定义,也可以让编译器提供
  • 如果定义了构造函数,编译器就不会创建默认构造函数。这种情况下如果需要默认构造函数,需要自己定义。

同样的,如果没有定义析构函数,编译器也将创建一个函数体为空,不执行任何操作的析构函数。

程序清单8.2 NewTricycle.cpp

#include <iostream>

class Tricycle
{
public:
    Tricycle(int initialAge);
    ~Tricycle();
    int getSpeed();
    void setSpeed(int speed);
    void pedal();
    void brake();

private:
    int speed;
};

Tricycle::Tricycle(int initialAge)
{
    setSpeed(initialAge);
}

Tricycle::~Tricycle()
{
    std::cout << "Destructed\n";
}

int Tricycle::getSpeed()
{
    return speed;
}

void Tricycle::setSpeed(int newSpeed)
{
    if (newSpeed >= 0)
    {
        speed = newSpeed;
    }
}

void Tricycle::pedal()
{
    setSpeed(speed + 1);
    std::cout << "\nPedaling;tricycle speed " << speed << " mph\n";
}

void Tricycle::brake()
{
    setSpeed(speed - 1);
    std::cout << "\nBraking;tricycle speed " << speed << " mph\n";
}

int main()
{
    Tricycle wichita(5);
    wichita.pedal();
    wichita.pedal();
    wichita.brake();
    wichita.brake();
    wichita.brake();
    return 0;
}

第九章 高级类

9.1 const成员函数

如果使用关键字const将成员函数声明为常量函数(常函数),则表明它不会修改任何类成员的值。要将函数声明为常量函数,可在括号后面添加关键字const:

void displayPage() const;

如果将函数声明为常函数,但其实现修改了成员,编译器就会报错。

对于那些不用来修改类成员的函数,应尽可能将其声明为常函数,这是一种良好的编程习惯。这样可让编译器发现您对成员变量的无意间修改,避免在运行阶段出现这些错误。

9.2接口和实现

9.3组织类声明和函数定义

在c++的源代码中,类的定义和实现通常是分开的。您在类中声明的每个函数都必须有定义。与常规函数一样,类函数也包含函数头与函数体。

定义必须放在编译器能够找到的文件中,大多数c++编译器都要求这种文件的扩展名为.cpp

虽然可以将声明放在源代码文件中,但是大多数程序员采取的做法是,将声明放在头文件中,其文件名与源代码文件相同,但扩展名为.hpp(或是不那么常见的.h或.hp)

因此,如果您将Tricycle类的声明放在文件Tricycle.hpp中,那么类函数的定义位于文件Tricycle.cpp中。通过使用预处理编译指令,可在.cpp文件中包含头文件。

#include "Tricycle.hpp"

将他们分开的原因是,类的客户不关心实现细节,他们需要知道的信息都包含在头文件中。

9.4内联实现

可将常规函数声明为内联,同样,也可将成员函数声明为内联的,为此,需要在返回类型前面指定关键字inline,如下所示:

inline int Tricycle::getSpeed()
{
 return speed;
}

也可将函数定义放在类声明中(也就是写在里面),这样函数将自动变成内联的,如下所示:

class Tricycle
{
public:
 int getSpeed() const 
 {
     return speed;
 }
 void setSpeed(int newSpeed);
};

程序清单9.1与9.2实现了Tricycle类的声明与实现的分离:

程序清单9.1 Tricycle.hpp

#include <iostream>

class Tricycle
{
public:
    Tricycle(int initialAge);
    ~Tricycle();
    int getSpeed() const { return speed; }
    void setSpeed(int speed);
    void pedal()
    {
        setSpeed(speed + 1);
        std::cout << "\nPedaling;tricycle speed " << speed << " mph\n";
    }
    void brake()
    {
        setSpeed(speed - 1);
        std::cout << "\nBraking;tricycle speed " << speed << " mph\n";
    }

private:
    int speed;
};

程序清单9.2 Tricycle.cpp

#include "Tricycle.hpp"

Tricycle::Tricycle(int initialAge)
{
    setSpeed(initialAge);
}
Tricycle::~Tricycle()
{
    std::cout << "Destructed\n";
}
void Tricycle::setSpeed(int newSpeed)
{
    if (newSpeed >= 0)
    {
        speed = newSpeed;
    }
}

int main()
{
    Tricycle wichita(5);
    wichita.pedal();
    wichita.pedal();
    wichita.brake();
    wichita.brake();
    wichita.brake();
    return 0;
}

可以看到运行效果与笔记8 程序清单8.2是一样的。

9.5将其他类用作成员数据的类

创建复杂类时,经常将简单类作为其成员

程序清单9.3与9.4为例:(其实书上给的这个例子里面太多的初始化方法,俺表示无语)

程序清单9.3 Rectangle.hpp

#include <iostream>

class Point
{
public:
    void setX(int newX) { x = newX; }
    void setY(int newY) { y = newY; }
    int getX() const { return x; }
    int getY() const { return y; }

private:
    int x;
    int y;
};

class Rectangle
{
public:
Rectangle(int newTop,int newLeft,int newBottom,int newRight);
~Rectangle(){std::cout << "Destructed\n";}

int getTop() const {return top;}
int getLeft() const {return left;}
int getBottom() const {return bottom;}
int getRight() const {return right;}

Point getUpperLeft() const {return upperLeft;}
Point getLowerLeft() const {return lowerLeft;}
Point getUpperRight() const {return upperRight;}
Point getLowerRight() const {return lowerRight;}

void setUpperLeft(Point location);
void setLowerLeft(Point location);
void setUpperRight(Point location);
void setLowerRight(Point location);

void setTop(int newTop);
void setLeft(int newLeft);
void setBottom(int newBottom);
void setRight(int newRight);

int getArea() const;

private:
Point upperLeft;
Point upperRight;
Point lowerLeft;
Point lowerRight;
int top;
int left;
int bottom;
int right;
};

程序清单9.4 Rectangle.cpp

#include "Rectangle.hpp"

Rectangle::Rectangle(int newTop,int newLeft,int newBottom,int newRight)
{
    top=newTop;
    left=newLeft;
    bottom=newBottom;
    right=newRight;

    upperLeft.setX(left);
    upperLeft.setY(top);

    upperRight.setX(right);
    upperRight.setY(top);

    lowerLeft.setX(left);
    lowerLeft.setY(bottom);

    lowerRight.setX(right);
    lowerRight.setY(bottom);
}

void Rectangle::setUpperLeft(Point location)
{
    upperLeft = location;
    upperRight.setY(location.getY());
    lowerLeft.setX(location.getX());
    top = location.getY();
    left = location.getX();
}

void Rectangle::setLowerLeft(Point location)
{
    lowerLeft=location;
    lowerRight.setY(location.getY());
    upperLeft.setX(location.getX());
    bottom = location.getY();
    left = location.getX();
}

void Rectangle::setLowerRight(Point location)
{
    lowerRight=location;
    lowerLeft.setY(location.getY());
    upperRight.setX(location.getX());
    bottom = location.getY();
    right = location.getX();
}

void Rectangle::setUpperRight(Point location)
{
    upperRight = location;
    upperLeft.setY(location.getY());
    lowerRight.setX(location.getX());
    top = location.getY();
    right = location.getX();
}

void Rectangle::setTop(int newTop)
{
    top = newTop;
    upperLeft.setY(top);
    upperRight.setY(top);
}

void Rectangle::setLeft(int newLeft)
{
    left = newLeft;
    upperLeft.setX(left);
    lowerLeft.setX(left);
}

void Rectangle::setBottom(int newBottom)
{
    bottom = newBottom;
    lowerLeft.setY(bottom);
    lowerRight.setY(bottom);
}

void Rectangle::setRight(int newRight)
{
    right=newRight;
    upperRight.setX(right);
    lowerRight.setX(right);
}

int Rectangle::getArea() const
{
    int width = right - left;
    int height = top - bottom;
    return (width*height);
}

int main()
{
    Rectangle myRectangle(100,20,50,80);
    int area = myRectangle.getArea();
    std::cout<<"Area: "<<area<<std::endl;
    std::cout<<"Upper Left X Coordinate: ";
    std::cout<<myRectangle.getUpperLeft().getX()<<std::endl;
}

第十章 创建指针

10.1理解指针及其用途

变量是可存储一个值的对象:整型变量存储一个数字,字符变量存储一个字母,而指针是存储内存地址的变量

计算机内存是存储变量值的地方。根据约定,计算机内存被划分成按顺序编号的内存单元,每个内存单元都有对应的地址。内存中,不管其类型是什么,每个变量都位于特定的地址处。

内存编址方案随计算机而异。通常,程序员无须知道变量地址,如果想要获取地址信息,可使用地址运算符&

程序清单10.1 Address.cpp

#include<iostream>

int main()
{
    unsigned short shortVar = 5;
    unsigned long longVar = 65535;
    long sVar = -65535;

    std::cout<<"shortVar:\t"<<shortVar;
    std::cout<<"\tAddress of shortVar:\t"<<&shortVar<<"\n";
    std::cout<<"longVar:\t"<<longVar;
    std::cout<<"\tAddress of longVar:\t"<<&longVar<<"\n";
    std::cout<<"sVar:\t"<<sVar;
    std::cout<<"\tAddress of sVar:\t"<<&sVar<<"\n";

}

(地址默认以16进制表示法输出的)

您运行该程序时,变量的地址将不同,因为这取决于内存中存储的其他内容以及可用的内存有多少。

在指针中存储地址

每个变量都有地址,即使不知道变量的具体地址,也可将该地址存储在指针变量中。

int howOld = 50;
int* pAge = nullptr;//初始化一个int型空指针变量,这样能更明显看出来pAge类型是int*,但c/c++的标准写法是int *pAge
pAge = &howOld;//将howOld的地址取出来放入指针变量pAge中

间接运算符

间接运算符(*)又被称为解引用运算符。对指针解除引用时,将获取指针存储的地址处的值。

int howOld = 50;
int* pAge = &howOld;
int yourAge;
yourAge = *pAge;//yourAge的值变成了50
*pAge = 10;//howOld的值变成了10,而yourAge的值还是50

指针pAge前面的间接运算符(*)表示“存储在......处的值”。这条赋值语句的意思是,从pAge指向的地址处获取值,并将其赋给yourAge。看待这条语句的另一种方式是,不影响指针,而是影响指针指向的内容(比如上面最后一条语句)。

使用指针操作数据(其实上面那个例子就是)

程序清单10.2 Pointer.cpp

#include <iostream>

int main()
{
    int myAge;
    int *pAge = nullptr;

    myAge = 5;
    pAge = &myAge;
    std::cout << "myAge: " << myAge << "\n";
    std::cout << "*pAge: " << *pAge << "\n\n";

    std::cout << "*pAge = 7\n";
    *pAge = 7;
    std::cout << "myAge: " << myAge << "\n";
    std::cout << "*pAge: " << *pAge << "\n\n";

    std::cout << "myAge = 9\n";
    myAge = 9;
    std::cout << "myAge: " << myAge << "\n";
    std::cout << "*pAge: " << *pAge << "\n";
}

查看存储在指针中的地址:

程序清单10.3 PointerCheck.cpp

#include <iostream>

int main()
{
    unsigned short int myAge = 5, yourAge = 10;
    unsigned short int *pAge = &myAge;

    std::cout << "pAge: " << pAge << "\n";
    std::cout << "*pAge: " << *pAge << "\n";

    pAge = &yourAge;
    std::cout << "after reassign the pAge point to yourAge : "
              << "\n";
    std::cout << "pAge: " << pAge << "\n";
    std::cout << "*pAge: " << *pAge << "\n";

    return 0;
}

为何使用指针

熟悉指针的语法后,便可将其用于其他用途了,指针最长用于完成如下三项任务:

  • 管理堆中的数据;
  • 访问类的成员数据和成员函数;
  • 按引用将变量传递给函数

10.2堆和栈

(这部分其实如果有一点数据结构或者操作系统基础更好)

程序员通常需要处理下述五个内存区域

  • 全局名称空间
  • 寄存器
  • 代码空间

局部变量和函数参数存储在栈中,代码当然在代码空间中,而全局变量在全局名称空间中。寄存器用于内存管理,如跟踪栈顶和指令指针,几乎余下的所有内存都分配给了堆,堆有时也被称为自由存储区。

局部变量的局限性是不会持久化,函数返回时,局部变量将被丢弃。全局变量解决了这种问题,但代价是在整个程序中都能访问它,这导致代码容易出现bug,难以理解与维护。将数据放在堆中可解决这两个问题。

每当函数返回时,都会清理栈(实际上,开始调用函数时,栈空间进行压栈操作;函数调用完时,栈空间进行出栈操作)。此时,所有的局部变量都不在作用域内,从而从栈中删除。只有到程序结束后才会清理堆,因此使用完预留的内存后,您需要负责将其释放(手动GC)。让不再需要的信息留在堆中称为内存泄露(垃圾滞留)

堆的优点在于,在显示释放前,您预留的内存始终可用。如果在函数中预留堆中的内存,在函数返回后,该内存仍可用。

以这种方式(而不是全局变量)访问内存的优点是,只有有权访问指针的函数才能访问它指向的数据。这提供了控制严密的数据接口,消除了函数意外修改数据的问题。

关键字new

在c++中,使用new关键字分配堆中的内存,并在其后指定要为之分配内存的对象的类型,让编译器知道需要多少内存。比如new int分配4字节内存。

关键字new返回一个内存地址,必须将其赋给指针。

int *pPointer = new int;//指针pPointer将指向堆中的一个int变量
*pPointer = 72;//将72赋值给pPointer指向的堆内存变量

关键字delete

使用好了分配的内存区域后,必须对指针调用delete,将内存归还给堆空间。

delete pPointer;

对指针调用delete时,将释放它指向的内存。如果再次对该指针调用delete,就会导致程序崩溃(delete野指针)。删除指针时,应将其设置为nullptr,对空指针调用delete是安全的。

Animal *pDog = new Animal;
delete pDog;//释放内存
pDog = nullptr;//设置空指针
delete pDog;//安全行为

程序清单10.4 Heap.cpp

#include <iostream>

int main()
{
    int localVariable = 5;
    int *pLocal = &localVariable;
    int *pHeap = new int;
    if (pHeap == nullptr)
    {
        std::cout << "Error! No memory for pHeap!!";
        return 1;
    }
    *pHeap = 7;
    std::cout << "localVariable: " << localVariable << std::endl;
    std::cout << "*pLocal: " << *pLocal << std::endl;
    std::cout << "*pHeap: " << *pHeap << std::endl;
    delete pHeap; //此处只是释放了堆中new分配的内存,并没有删除指针,所以下面可以接着用。
    pHeap = new int;
    if (pHeap == nullptr)
    {
        std::cout << "Error! No memory for pHeap!!";
        return 1;
    }
    *pHeap = 9;
    std::cout << "*pHeap: " << *pHeap << std::endl;
    delete pHeap; //再次释放new出来的内存
    return 0;
}

另一种可能无意间导致内存泄露的情形是,没有释放指针指向的内存就给它重新赋值。

int *pPointer = new int;
*pPointer = 72;
pPointer = new int;
*pPointer = 50;//指针变量指向了一个新new出来的存有50的int型变量,但是之前那个存有72的堆内存变量还没被释放,也就造成了内存泄露

上述代码应该修改成这样:

int *pPointer = new int;
*pPointer = 72;
delete pPointer;
pPointer = new int;
*pPointer = 50;

也就是说一个不存在内存泄露的c++程序至少其new与delete是成对的,或者说是数量相等的。(这可苦了c艹程序员咯!)

空指针常量:

在较早的c++版本中,使用0或者NULL来设置指针为空值。

int *pBuffer = 0;
int *pBuffer = NULL;

但是其实因为定义NULL的语句是一个预处理宏:

#define NULL 0

所以其实NULL和0一个意思,上面两句也是一个意思。

但是当某个函数进行了重载,参数有指针类型也有int型的时候,传了一个值为0的空值指针进去,这个时候会出现二义性:

void displayBuffer(char*);//这个函数的参数是char型指针变量
void displayBuffer(int);

如果将空指针作为参数去调用,那么会调用displayBuffer(int),这个时候就会造成运行与预期不同。

所以才应该使用关键字nullptr

int *pBuffer = nullptr;

程序清单10.5 Swapper.cpp

#include <iostream>

int main()
{
    int value1 = 12500;
    int value2 = 1700;
    int *pointer2 = nullptr;
    pointer2 = &value2;
    value1 = *pointer2;
    pointer2 = 0;
    std::cout << "value = " << value1 << "\n";

    return 0;
}

第十一章 开发高级指针

11.1在堆中创建对象

实际上,类就是对象的类型,对象也是一种变量,所以你可以在堆中创建int型变量,自然也就能创建自定义型变量。

Cat *pCat = new Cat;

这将调用默认构造函数(无参构造函数),每当在堆或栈中创建对象时,都将调用构造函数。

11.2删除对象

对指向堆中对象的指针调用delete时,将调用对象的析构函数,然后释放内存。

程序清单11.1 HeapCreator.cpp

#include <iostream>

class SimpleCat
{
public:
    SimpleCat()
    {
        std::cout << "Constructor called\n";
        itsAge = 1;
    }
    ~SimpleCat()
    {
        std::cout << "Destructor called\n";
    }

private:
    int itsAge;
};

int main()
{
    std::cout << "SimpleCat simpleCat ...\n";
    SimpleCat simpleCat;

    std::cout << "SimpleCat *pRags = new SimpleCat ...\n";
    SimpleCat *pRags = new SimpleCat;

    std::cout << "delete pRags ...\n";
    delete pRags;

    std::cout << "Exiting, watch simpleCat go ...\n";
    return 0;
}

这里最后一个Destructor called是因为main()函数结束时,simpleCat对象不再在作用域中,所以编译器调用其析构函数。

11.3使用指针访问成员

方法一(解引用运算符):

(*pRags).getAge();

方法二(指向运算符->):

pRags->getAge();

程序清单11.2 HeapAccessor.cpp

#include <iostream>

class SimpleCat
{
public:
    SimpleCat()
    {
        itsAge = 2;
    }
    ~SimpleCat()
    {
        std::cout << "Destructor called\n";
    }
    int getAge() const { return itsAge; }
    void setAge(int age) { itsAge = age; }

private:
    int itsAge;
};

int main()
{
    SimpleCat *simpleCat = new SimpleCat;

    std::cout << "simpleCat is " << (*simpleCat).getAge() << " years old"
              << "\n";
    simpleCat->setAge(5);
    std::cout << "simpleCat is " << simpleCat->getAge() << " years old"
              << "\n";

    return 0;
}

11.4堆中的数据成员

类可能有一个或多个数据成员为指针,并指向堆中的对象。可在构造函数或成员函数中分配内存,并在析构函数中释放内存。

程序清单11.3 DataMember.cpp

#include <iostream>

class SimpleCat
{
public:
    SimpleCat()
    {
        itsAge = new int(2);
        itsWeight = new int(5);
    }
    ~SimpleCat()
    {
        delete itsAge;
        delete itsWeight;
    }
    int getAge() const { return *itsAge; }
    void setAge(int age) { *itsAge = age; }
    int getWeight() const { return *itsWeight; }
    void setWeight(int weight) { *itsWeight = weight; }

private:
    int *itsAge;
    int *itsWeight;
};

int main()
{
    SimpleCat *simpleCat = new SimpleCat;

    std::cout << "simpleCat is " << simpleCat->getAge() << " years old"
              << "\n";
    simpleCat->setAge(5);
    std::cout << "simpleCat is " << simpleCat->getAge() << " years old"
              << "\n";

    return 0;
}

11.5this指针

每个类成员函数都有一个隐藏的参数——this指针,它指向用于调用函数的对象。

通常,在成员函数中,无需使用this指针来访问当前对象的成员变量,如果愿意,可以显示地使用this指针。

程序清单11.4 This.cpp

#include <iostream>

class Rectangle
{
private:
    int itsLength;
    int itsWidth;

public:
    Rectangle();
    ~Rectangle();
    void setLength(int length) { this->itsLength = length; }
    int getLength() const { return this->itsLength; }
    void setWidth(int width) { this->itsWidth = width; }
    int getWidth() const { return this->itsWidth; }
};

Rectangle::Rectangle()
{
    itsWidth = 5;
    itsLength = 10;
}

Rectangle::~Rectangle()
{
}

int main()
{
    Rectangle theRect;
    std::cout << "theRect is " << theRect.getLength() << " feet long." << std::endl;
    std::cout << "theRect is " << theRect.getWidth() << " feet wide." << std::endl;
    theRect.setLength(20);
    theRect.setWidth(10);
    std::cout << "theRect is " << theRect.getLength() << " feet long." << std::endl;
    std::cout << "theRect is " << theRect.getWidth() << " feet wide." << std::endl;
    return 0;
}

11.6悬垂指针

悬垂指针又称为野指针或者迷失指针,指的是对指针调用了delete(释放其指向的内存)之后,没有重新赋值(即没有重新初始化)就开始被使用的指针。

实际上上章笔记中delete关键字时就已经提到野指针的危害。所以进行delete之后应该重新new赋值或者设置为nullptr。

11.7const指针

声明指针时,可在类型前、类型后或者两个地方都使用const。

const int *pOne;//指向常量的指针
int * const pTwo;//常量指针
const int * const pThree;//指向常量的常量指针

三条语句意义各不相同,三个指针类型也各不相同。

pOne是指向整型常量的指针,也就是编译器默认它指向的是一个常量(虽然可能不是),所以不能通过这个指针来更改所指向的常量(编译器认为是常量但不一定是)的值,比如*pOne = 5;编译器就会报错。

 int one = 10;
 const int * pOne = &one;
 *pOne = 5;//报错,表达式必须是可修改的左值,但此时*pOne被认为不可修改

pTwo是指向整型的常量指针,可以修改指向的整型变量,但是pTwo不能指向其他变量。

 int two = 20;
 int * const pTwo = &two;
 *pTwo = 15;
 pTwo = &one;//报错,不能指向别的变量

pThree是一个指向整型常量的常量指针,不能修改它指向的值,也不能让它指向其他变量。

	int three = 30;
 const int * const pThree = &three;
 pThree = &one;//报错,不能指向别的变量
 *pThree = 25;//报错,此时*pThree被认为不可修改

完整代码:(注释起来的是报错的)

#include <iostream>

int main()
{
    int one = 10;
    const int * pOne = &one;
//    *pOne = 5;

    int two = 20;
    int * const pTwo = &two;
    *pTwo = 15;
//    pTwo = &one;

    int three = 30;
    const int * const pThree = &three;
//    pThree = &one;
//    *pThree = 25;

    std::cout<<"one: "<<one<<" *pOne: "<<*pOne<<std::endl;
    std::cout<<"two: "<<two<<" *pTwo: "<<*pTwo<<std::endl;
    std::cout<<"three: "<<three<<" *pThree: "<<*pThree<<std::endl;
    return 0;
}

11.8const指针与const成员函数

程序清单11.5 ConstPointer.cpp

#include <iostream>

class Rectangle
{
private:
    int itsLength;
    int itsWidth;

public:
    Rectangle();
    ~Rectangle();
    void setLength(int length) { itsLength = length; }
    int getLength() const { return itsLength; }
    void setWidth(int width) { itsWidth = width; }
    int getWidth() const { return itsWidth; }
};

Rectangle::Rectangle() : itsWidth(5), itsLength(10) //初始化列表
{
}

Rectangle::~Rectangle() {}

int main()
{
    Rectangle *pRect = new Rectangle;
    const Rectangle *pConstRect = new Rectangle; //pConstRect为指向Rectangle常量型对象的指针
    Rectangle *const pConstPtr = new Rectangle;  //pConstPtr为指向Rectangle型对象的常量指针

    std::cout << "pRect width: " << pRect->getWidth() << " feet\n";
    std::cout << "pConstRect width: " << pConstRect->getWidth() << " feet\n";
    std::cout << "pConstPtr width: " << pConstPtr->getWidth() << " feet\n";

    pRect->setWidth(10);
    //pConstRect->setWidth(10);
    pConstPtr->setWidth(10);

    std::cout << "pRect width: " << pRect->getWidth() << " feet\n";
    std::cout << "pConstRect width: " << pConstRect->getWidth() << " feet\n";
    std::cout << "pConstPtr width: " << pConstPtr->getWidth() << " feet\n";

    return 0;
}

第12章 创建引用

12.1什么是引用

引用是一个别名。创建引用时,使用另一个对象(目标)的名称来初始化它,从此以后该引用就像是目标的另一个名称,对引用执行的任何操作实际上针对的就是目标。

有些书上说引用就是指针,这不正确。虽然引用常常是使用指针实现的,但是只有编译器开发人员关心这一点,作为程序员,必须区分这两种概念。

指针是存储另一个对象的地址的变量,而引用时对象的别名。

12.2创建引用

要创建引用,需要指定目标对象的类型、引用运算符(&)和引用名。

程序清单12.1 Reference.cpp

#include <iostream>

int main()
{
    int intOne;
    int &rSomeRef = intOne;

    intOne = 5;
    std::cout << "intOne: " << intOne << std::endl;
    std::cout << "rSomeRef: " << rSomeRef << std::endl;

    rSomeRef = 7;
    std::cout << "intOne: " << intOne << std::endl;
    std::cout << "rSomeRef: " << rSomeRef << std::endl;
    return 0;
}

12.3将地址运算符用于引用

如果请求返回引用的地址,就将返回它指向的目标的地址。这是引用的特征:他们是目标的别名

程序清单12.2 Reference2.cpp

#include <iostream>

int main()
{
    int intOne;
    int &rSomeRef = intOne;

    intOne = 5;
    std::cout << "intOne: " << intOne << std::endl;
    std::cout << "rSomeRef: " << rSomeRef << std::endl;

    std::cout << "&intOne: " << &intOne << std::endl;
    std::cout << "&rSomeRef: " << &rSomeRef << std::endl;
    return 0;
}

通常,使用引用时,不将地址运算符用于它,而像使用目标变量那样使用引用。

程序清单12.3 Assignment.cpp

#include <iostream>

int main()
{
    int intOne;
    int &rSomeRef = intOne;

    intOne = 5;
    std::cout << "intOne:\t" << intOne << std::endl;
    std::cout << "rSomeRef:\t" << rSomeRef << std::endl;
    std::cout << "&intOne:\t" << &intOne << std::endl;
    std::cout << "&rSomeRef:\t" << &rSomeRef << std::endl;

    int intTwo = 8;
    rSomeRef = intTwo;
    std::cout << "\nintOne:\t" << intOne << std::endl;
    std::cout << "intTwo:\t" << intTwo << std::endl;
    std::cout << "rSomeRef:\t" << rSomeRef << std::endl;
    std::cout << "&intOne:\t" << &intOne << std::endl;
    std::cout << "&intTwo:\t" << &intTwo << std::endl;
    std::cout << "&rSomeRef:\t" << &rSomeRef << std::endl;
    return 0;
}

12.4可引用的目标

因为对象也是一种变量,所以可引用任何对象,包括用户定义的对象。可以像使用对象那样使用对象的引用:访问成员数据和成员函数时,使用类成员访问运算符(.)与内置类型的引用一样,指向对象的引用也是对象的别名。

12.5空指针和空引用

指针未初始化或被删除时,应将其赋为nullptr,但引用不一样,引用不能为空,让引用指向空对象的程序是非法的。

12.6按引用传递函数参数

前面知道了函数的两个局限性:参数按值传递;return语句只能返回一个值。

通过将值按引用传递给函数,可消除这两种局限性。在c++中,按引用传递时通过两种方式完成的:使用指针和使用引用。他们的语法不同,但效果相同:不是在函数作用域内创建备份(也就是不是值拷贝),而是将原始对象传递给函数。

程序清单12.4 ValuePasser.cpp

#include <iostream>
void swap(int x, int y);

int main()
{
    int x = 5, y = 10;
    std::cout << "Main. Before swap,x: " << x << " y: " << y << std::endl;
    swap(x, y);
    std::cout << "Main. Before swap,x: " << x << " y: " << y << std::endl;
    return 0;
}

void swap(int x, int y)
{
    int temp;
    std::cout << "Swap. Before swap,x: " << x << " y: " << y << std::endl;
    temp = x;
    x = y;
    y = temp;
    std::cout << "Swap. After swap,x: " << x << " y: " << y << std::endl;
}

main()中的值都没变,可见值拷贝并不能改变原参的值

使用指针实现swap()

程序清单12.5 PointerSwap.cpp

#include <iostream>
void swap(int *x, int *y);

int main()
{
    int x = 5, y = 10;
    std::cout << "Main. Before swap,x: " << x << " y: " << y << std::endl;
    swap(&x, &y);//将地址作为参数传递
    std::cout << "Main. Before swap,x: " << x << " y: " << y << std::endl;
    return 0;
}

void swap(int *px, int *py)//参数声明为指针
{
    int temp;
    std::cout << "Swap. Before swap,*px: " << *px << " *py: " << *py << std::endl;
    temp = *px;
    *px = *py;
    *py = temp;
    std::cout << "Swap. After swap,*px: " << *px << " *py: " << *py << std::endl;
}

使用引用实现swap()

c++的目标之一时,避免函数的调用者操心函数的工作原理,而将注意力放在函数的功能和返回值上。传递指针将负担转嫁给了调用方,而这种负担原本不应该由调用方来承担:调用方必须知道将要交换的对象的地址传入。

明白引用语法的负担应由函数的实现方承担。为此,可使用引用。

程序清单12.6 ReferenceSwap.cpp

#include <iostream>
void swap(int &x, int &y);

int main()
{
    int x = 5, y = 10;
    std::cout << "Main. Before swap,x: " << x << " y: " << y << std::endl;
    swap(x, y);
    std::cout << "Main. Before swap,x: " << x << " y: " << y << std::endl;
    return 0;
}

void swap(int &rx, int &ry)//参数声明为引用
{
    int temp;
    std::cout << "Swap. Before swap,rx: " << rx << " ry: " << ry << std::endl;
    temp = rx;
    rx = ry;
    ry = temp;
    std::cout << "Swap. After swap,rx: " << rx << " ry: " << ry << std::endl;
}

可见两种方式(指针传地址、引用传原参)达到的效果是一样的,但是引用传递中,调用方只需传递变量,且在函数内部,需要使用的特殊符号减少了,降低了程序的复杂性。引用将常规变量方便而易于使用的特点和指针的强大融为一体。

12.7理解函数头和原型

函数原型的另一个重要用途:通过查看原型中声明的参数(函数原型通常放在头文件中),程序员知道swap()的参数是按指针还是引用传递的,从而将正确调用他们。

在c++中,类的使用者(其他类中使用该类的函数)依赖于头文件来获悉需要的所有信息。头文件相当于类或函数的接口,而实际实现对使用者是隐藏的。这让程序员能够将主要精力放在要解决的问题上,而使用类或函数时无需关心它是如何实现的。

12.8返回多个值

(哦,这说和没说是一样的,还是只能return一个东西)

一种解决办法是将多个对象按引用传入函数,然后在函数中将正确的值赋给这些对象。由于按引用传递让函数能够修改原始对象,因此这相当于让函数能够返回多项信息。这种函数未使用函数的返回值,可将其(指返回值)用于报告错误。

另一种办法是使用指针或引用来实现。

程序清单12.7 ReturnPointer.cpp

#include <iostream>

short factor(int, int *, int *);

int main()
{
    int number, squared, cubed;
    short error;
    std::cout << "Enter a number(0 - 20): ";
    std::cin >> number;

    error = factor(number, &squared, &cubed);

    if (!error)
    {
        std::cout << "number: " << number << "\n";
        std::cout << "square: " << squared << "\n";
        std::cout << "cubed: " << cubed << "\n";
    }
    else
        std::cout << "Error encountered!!\n";
    return 0;
}

short factor(int n, int *pSquared, int *pCubed)
{
    short vaule = 0;
    if (n > 20)
    {
        vaule = 1;
    }
    else
    {
        *pSquared = n * n;
        *pCubed = n * n * n;
        vaule = 0;
    }
    return vaule;
}

按引用返回值

虽然程序ReturnPointer.cpp可行,但是如果使用引用而不是指针,将更容易理解和维护。

程序清单12.8 ReturnReference.cpp

#include <iostream>

enum ERR_CODE//枚举
{
    SUCCESS,
    ERROR
};

ERR_CODE factor(int, int &, int &);

int main()
{
    int number, squared, cubed;
    ERR_CODE result;

    std::cout << "Enter a number(0 - 20): ";
    std::cin >> number;

    result = factor(number, squared, cubed);

    if (result == SUCCESS)
    {
        std::cout << "number: " << number << "\n";
        std::cout << "square: " << squared << "\n";
        std::cout << "cubed: " << cubed << "\n";
    }
    else
        std::cout << "Error encountered!!\n";
    return 0;
}

ERR_CODE factor(int n, int &pSquared, int &pCubed)
{
    short vaule = 0;
    if (n > 20)
    {
        return ERROR;
    }
    else
    {
        pSquared = n * n;
        pCubed = n * n * n;
        return SUCCESS;
    }
}

第十三章 高级引用和指针

13.1按引用传递以提高效率

​ 每次将值按对象传入函数是,都将创建该对象的一个备份。每次按值从函数返回一个对象时,也将创建其备份。

​ 对于用户创建的大型对象,备份的代价很高。这将增加程序占用的内存量,而程序的运行速度将更慢。

​ 在栈中,用户创建的对象的大小为其成员变量的大小之和,而每个成员变量本身也可能是用户创建的对象。从性能和内存耗用方面说,将如此大的结构传入栈中的代价非常高。

​ 当然,还有其它代价。对于您创建的类,每次创建备份时,编译器都将调用一个特殊的构造函数:复制构造函数(拷贝构造函数)。

​ 对于大型对象,调用这些构造函数和析构函数在速度和内存耗用方面的代价非常高。

程序清单13.1 ObjectRef.cpp

#include <iostream>

class SimpleCat {
public:
  SimpleCat();            //默认构造函数
  SimpleCat(SimpleCat &); //拷贝构造函数
  ~SimpleCat();
};

SimpleCat::SimpleCat() {
  std::cout << "Simple Cat Constructor ..." << std::endl;
}

SimpleCat::SimpleCat(SimpleCat &) {
  std::cout << "Simple Cat Copy Constructor ..." << std::endl;
}

SimpleCat::~SimpleCat() {
  std::cout << "Simple Cat Destructor ..." << std::endl;
}

SimpleCat FunctionOne(SimpleCat theCat);
SimpleCat *FunctionTwo(SimpleCat *theCat);

int main() {
  std::cout << "Making a Cat ..." << std::endl;
  SimpleCat simpleCat;
  std::cout << "Calling FunctionOne..." << std::endl;
  FunctionOne(simpleCat);
  std::cout << "Calling FunctionTwo..." << std::endl;
  FunctionTwo(&simpleCat);
  return 0;
}

SimpleCat FunctionOne(SimpleCat theCat) {
  std::cout << "Function One. Returning ..." << std::endl;
  return theCat;
}
SimpleCat *FunctionTwo(SimpleCat *theCat) {
  std::cout << "Function Two. Returning ..." << std::endl;
  return theCat;
}

​ 可以看到程序中FunctionOne按值传递,FunctionTwo按址传递,FunctionOne的调用会产生一次拷贝构造,返回值是SimpleCat类型,所以也会产生一次拷贝构造,然后FunctionOne结束时,两个拷贝出来的对象(一个是调用时产生,一个是返回时产生)就会被析构函数进行析构。

​ 调用FunctionTwo,因为参数是按引用传递,所以不会进行复制备份,也就不会调用拷贝构造函数,所以也就不会有输出。所以结果中,FunctionTwo所触发的输出语句仅有Function Two. Returning ...一句,且并未调用拷贝构造和析构函数。

13.2传递const指针

这部分其实前面十一章的笔记有提到。

​ 虽然将指针传递给函数效率更高(比如上面的程序),但这也使得对象有被修改的风险。按值传递虽然效率较低,但是只是将复制品传递,所以并不会影响到原品,也就较按址传递多了一层保护。

​ 要同时获得按值传递的安全性和按址传递的效率,解决的办法是传递一个指向该类常量的指针(也可为常量的常量指针,即const 类型 *const 指针变量),这样就不能修改所指向对象的值(如果是常量的常量指针,不仅不能修改所指向对象的值,也不能修改自身的值,即不能指向其它对象)

程序清单13.2 ConstPasser.cpp

#include <iostream>

class SimpleCat {
private:
  int itsAge;

public:
  SimpleCat();
  SimpleCat(SimpleCat &);
  ~SimpleCat();

  int getAge() const { return itsAge; }
  void setAge(int age) { itsAge = age; }
};

SimpleCat::SimpleCat() {
  std::cout << "Simple Cat Constructor ..." << std::endl;
  itsAge = 1;
}

SimpleCat::SimpleCat(SimpleCat &) {
  std::cout << "Simple Cat Copy Constructor ..." << std::endl;
}

SimpleCat::~SimpleCat() {
  std::cout << "Simple Cat Destructor ..." << std::endl;
}

const SimpleCat *const
FunctionTwo(const SimpleCat *const simpleCat); //指向常量的常量指针

int main() {
  std::cout << "Making a cat ..." << std::endl;
  SimpleCat simpleCat;
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  int age = 5;
  simpleCat.setAge(age);
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  std::cout << "Calling FunctionTwo..." << std::endl;
  FunctionTwo(&simpleCat);
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  return 0;
}

const SimpleCat *const FunctionTwo(const SimpleCat *const simpleCat) {
  std::cout << "Function Two. Returning ..." << std::endl;
  std::cout << "simpleCat is now " << simpleCat->getAge() << " years old"
            << std::endl;
  // simpleCat->setAge(9);//报错,无法对const类型对象进行修改
  return simpleCat;
}

​ 如果将注释掉的代码去掉注释,程序则无法通过,所以可见将参数设置为常量的指针或者是常量的常量指针这种方式兼顾了址传递的高效与值传递的安全。

13.3作为指针替代品的引用

​ 将上一个程序重写,使用引用而非指针。

程序清单13.3 RefPasser.cpp

#include <iostream>

class SimpleCat {
private:
  int itsAge;

public:
  SimpleCat();
  SimpleCat(SimpleCat &);
  ~SimpleCat();

  int getAge() const { return itsAge; }
  void setAge(int age) { itsAge = age; }
};

SimpleCat::SimpleCat() {
  std::cout << "Simple Cat Constructor ..." << std::endl;
  itsAge = 1;
}

SimpleCat::SimpleCat(SimpleCat &) {
  std::cout << "Simple Cat Copy Constructor ..." << std::endl;
}

SimpleCat::~SimpleCat() {
  std::cout << "Simple Cat Destructor ..." << std::endl;
}

const SimpleCat &FunctionTwo(const SimpleCat &simpleCat); //指向常量的常量指针

int main() {
  std::cout << "Making a cat ..." << std::endl;
  SimpleCat simpleCat;
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  int age = 5;
  simpleCat.setAge(age);
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  std::cout << "Calling FunctionTwo..." << std::endl;
  FunctionTwo(simpleCat);
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  return 0;
}

const SimpleCat &FunctionTwo(const SimpleCat &simpleCat) {
  std::cout << "Function Two. Returning ..." << std::endl;
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  // simpleCat.setAge(9);//报错,无法对const类型对象进行修改
  return simpleCat;
}

​ 可见,改用引用效率和代价较指针没变,但是使用起来较指针简单。

13.4什么情况下使用引用以及什么情况下使用指针

​ 一般而言,c++程序员更喜欢使用引用而不是指针,因为他们更清晰,使用起来更容易。然而,引用不能重新赋值,如果需要依次指向不同的对象,就必须使用指针。引用不能为NULL,因此如果要指向的对象可能为NULL,就必须使用指针,而不能使用引用。如果要从堆中分配动态内存,就必须使用指针。

13.5指向对象的引用不在作用域内

​ c++程序员学会按引用传递后,常常会疯狂地使用这种方式。然而,过犹不及,别忘了,引用始终是另一个对象的别名,如果将引用传入或传出函数,务必自问:它是哪个对象的别名?当我使用引用时,这个对象还存在吗?

程序清单13.4 ReturnRef.cpp

#include <iostream>

class SimpleCat {
private:
  int itsAge;
  int itsWeight;

public:
  SimpleCat(int age, int weight);
  ~SimpleCat(){};
  int getAge() { return itsAge; }
  int getWeight() { return itsWeight; }
};

SimpleCat::SimpleCat(int age, int weight) : itsAge(age), itsWeight(weight) {}

SimpleCat &TheFunction();

int main() {
  SimpleCat &rCat = TheFunction();
  int age = rCat.getAge();
  std::cout << "rCat is " << age << " years old!" << std::endl;
  return 0;
}

SimpleCat &TheFunction() {
  SimpleCat simpleCat(5, 9);
  return simpleCat;
}

​ 这个程序一般编译器是会报错的,因为TheFunction()函数返回的对象引用是在TheFunction()函数内创建的,但是TheFunction()返回时,创建的这个对象是已经被销毁了的,也就是返回的引用指向了一个不存在的对象(空引用被禁止),从而被编译器禁止该程序运行。

13.6返回指向堆中对象的引用

​ 你是否认为:如果TheFunction()函数在堆中创建对象,这样,返回的时候这个对象就依然存在,也就自然解决了上面的空引用问题。

​ 这种方法的问题在于,使用完该对象后,如何释放为它分配的内存?

程序清单13.5 Leak.cpp

#include <iostream>

class SimpleCat {
private:
  int itsAge;
  int itsWeight;

public:
  SimpleCat(int age, int weight);
  ~SimpleCat(){};
  int getAge() { return itsAge; }
  int getWeight() { return itsWeight; }
};

SimpleCat::SimpleCat(int age, int weight) : itsAge(age), itsWeight(weight) {}

SimpleCat &TheFunction();

int main() {
  SimpleCat &rCat = TheFunction();
  int age = rCat.getAge();
  std::cout << "rCat is " << age << " years old!" << std::endl;
  std::cout << "&rCat is " << &rCat << std::endl;
  SimpleCat *pCat = &rCat;
  delete pCat; //原对象被释放了,这时候rCat不就成了空引用了吗?
  return 0;
}

SimpleCat &TheFunction() {
  SimpleCat *simpleCat = new SimpleCat(5, 9);
  std::cout << "SimpleCat: " << simpleCat << std::endl;
  return *simpleCat;
}

​ 不能对引用调用delete,一种聪明的解决方案是创建一个指针并将其指向该引用的对象的地址,这个时候调用delete也就能释放分配的内存,但是往后引用名不就成了空引用吗?虽然编译器检测不到且能正常运行,所以这就埋了一个雷,指不定什么时候炸了。(正如之前指出,引用必须始终是一个实际存在的对象的别名。如果它指向的是空对象,那么程序仍是非法的)

​ 对于这种问题,实际上有两种解决方案。一种是返回一个指针,这样可以在使用完该指针之后将其删除。为此,需要将返回值类型声明为指针而非引用,并返回该指针。

​ 另一种更好的解决方案是:在发出调用的函数中声明对象,然后将其按引用传递给TheFunction()。这种方法的优点是,分配内存的函数(发出调用的函数)也负责释放内存。

13.7谁拥有指针

​ 程序在堆中分配的内存时将返回一个指针。必须一直让某个指针指向这块内存,因为如果指针丢失(也就是没有指针指向它了),便无法释放该内存,进而导致内存泄露。

​ 在函数之间传递内存块时,其中一个函数“拥有”指针。通常,使用引用传递内存块中的值,而分配内存块的函数将负责释放它,但这是一个大致规则,并非不可打破。

​ 然而,让一个函数分配内存,而另一个函数释放很危险。在谁拥有指针方面不明确可能会导致两个问题:忘记删除指针或重复删除。无论哪种情况,都会给程序带来严重的问题。编写函数时,让其负责释放自己分配的内存是更安全的做法。

第十四章 高级函数

14.1重载成员函数

​ 函数可以进行重载,成员函数(成员方法)实质上也是一种函数,所以成员函数也可以进行重载。

程序清单14.1 Rectangle.cpp

#include <iostream>

class Rectangle
{
private:
    int width;
    int height;

public:
    Rectangle(int width, int height);
    ~Rectangle(){};

    void drawShape() const;
    void drawShape(int width, int height) const;
};

Rectangle::Rectangle(int width, int height)
{
    this->height = height;
    this->width = width;
}

void Rectangle::drawShape() const
{
    drawShape(width, height);
}

void Rectangle::drawShape(int width, int height) const
{
    for (int i = 0; i < height; i++)
    {
        for (int j = 0; j < width; j++)
        {
            std::cout << "*";
        }
        std::cout << std::endl;
    }
}

int main()
{
    Rectangle box(30, 5);
    std::cout << "drawShape():" << std::endl;
    box.drawShape();
    std::cout << "\ndrawShape(40,2):" << std::endl;
    box.drawShape(40, 2);
    return 0;
}

​ 编译器根据参数的类型和数值决定调用哪个版本。

14.2使用默认值

​ 常规函数可以有一个或多个默认值,类的成员函数也是如此。声明默认值的规则也相同。

程序清单14.2 Rectangle2.cpp

#include <iostream>

class Rectangle
{
private:
    int width;
    int height;

public:
    Rectangle(int weight, int height);
    ~Rectangle() {}
    void drawShape(int aWidth, int aHeight, bool useCurrentValue = false) const;
};

Rectangle::Rectangle(int width, int height)
{
    this->width = width;
    this->height = height;
}

void Rectangle::drawShape(int aWidth, int aHeight, bool useCurrentValue) const
{
    int printWidth = 0;
    int printHeight = 0;

    if (useCurrentValue == true)
    {
        printWidth = width;
        printHeight = height;
    }
    else
    {
        printWidth = aWidth;
        printHeight = aHeight;
    }

    for (int i = 0; i < printHeight; i++)
    {
        for (int j = 0; j < printWidth; j++)
        {
            std::cout << "*";
        }
        std::cout << std::endl;
    }
}

int main()
{
    Rectangle box(20, 5);
    std::cout << "drawShape(0,0,true) ..." << std::endl;
    box.drawShape(0, 0, true);
    std::cout << "drawShape(25,4) ..." << std::endl;
    box.drawShape(25, 4);
    return 0;
}

14.3初始化对象

​ 与成员函数一样,构造函数也可以重载。

​ 可以重载构造函数,但不能重载析构函数。析构函数的签名总是这样的:名称为类名前加~,且不接受任何参数。

​ 构造函数由两部分组成:初始化部分和函数体。可在初始化部分设置成员变量(即初始化列表),也可在构造函数的函数体内设置。初始化列表举例:

Tricycle::Tricycle():speed(5),wheelSize(12)
{
    //函数体
}

​ 初始化成员变量的效率比在函数体内给他们赋值高。

14.4复制构造函数

​ 又称拷贝构造函数

​ 除提供默认构造函数和析构函数之外,编译器还提供一个默认复制构造函数。每当创建对象的备份时,都将调用复制构造函数。

​ 按值将对象传入或传出函数时,都将创建对象的一个临时备份。如果对象是用户定义的,就将调用相应类的复制构造函数。

​ 所有复制构造函数都接受一个参数:一个引用,它指向所属类的对象。最好将该引用声明为常量,因为复制构造函数不用修改传入的对象,例如:Tricycle(const Tricycle &trike);

默认构造函数只作为参数传入的对象的每个成员变量复制到新对象中,这称为浅复制(浅拷贝)。虽然对大多数成员变量来说没问题,但是不适用于成员变量是指向堆中对象的指针这种情况。

浅复制将一个对象的成员变量的值复制到另一个对象中,这导致两个对象中的指针指向相同的内存地址。另一方面,深复制将堆内存中的值复制到新分配的堆内存中。

​ 浅复制使得两个或多个变量指向相同内存,如果当中一个不再在作用域内,就会导致导致调用析构函数释放分配的内存,而剩下的变量仍指向该内存,试图访问该内存将导致程序崩溃。

​ 对于这种问题,解决方案是自定义复制构造函数,并在复制时正确分配内存。

程序清单14.3 DeepCopy.cpp

#include <iostream>

class Tricycle
{
private:
    int *speed;

public:
    Tricycle();
    Tricycle(const Tricycle &);
    ~Tricycle();
    int getSpeed() const { return *speed; }
    void setSpeed(int newSpeed) { *speed = newSpeed; }
    void pedal();
    void brake();
};

Tricycle::Tricycle()
{
    speed = new int;
    *speed = 5;
}

Tricycle::Tricycle(const Tricycle &rhs)
{
    speed = new int;
    *speed = rhs.getSpeed();
}

Tricycle::~Tricycle()
{
    delete speed;
    speed = NULL;
}

void Tricycle::pedal()
{
    setSpeed(*speed + 1);
    std::cout << "\nPedaling " << getSpeed() << " mph" << std::endl;
}

void Tricycle::brake()
{
    setSpeed(*speed - 1);
    std::cout << "\nPedaling " << getSpeed() << " mph" << std::endl;
}

int main()
{
    std::cout << "Creating trike named wichita ...";
    Tricycle wichita;
    wichita.pedal();
    std::cout << "Creating trike named dallas ..." << std::endl;
    Tricycle dallas(wichita);
    std::cout << "wichita's speed: " << wichita.getSpeed() << std::endl;
    std::cout << "dallas's speed: " << dallas.getSpeed() << std::endl;
    std::cout << "setting wichita to 10 ..." << std::endl;
    wichita.setSpeed(10);
    std::cout << "wichita's speed: " << wichita.getSpeed() << std::endl;
    std::cout << "dallas's speed: " << dallas.getSpeed() << std::endl;
    return 0;
}

14.5编译阶段常量表达式

​ c++编译器竭尽所能地提高程序的运行速度——尽可能对编写的代码进行优化。一种存在效率提高空间的简单情况是将两个常量相加,如下所示:

const int decade = 10;
int year = 2016 + decade;

​ 2016与decade都是常量,所以编译器将计算这个表达式并将结果2026存储。因此,在编译器看来,就像是将2026赋给了year一样。

函数可使用const来返回常量值,如下所示:

const int getCentury()
{
    return 100;
}

你可能会认为如果调用语句int year = 2016 + getCentury();编译器会对表达式存在优化空间;虽然这个成员函数返回的是一个常量,但这个函数本身不是const的,它可能修改全局变量或调用非const成员函数。

常量表达式是c++新增的功能,用关键字constexpr表示:

constexpr int getCentury()
{
    return 100;
}

常量表达式必须返回一个表达式,而该表达式只能包含字面值、其他常量表达式或使用constexpr定义的变量。

程序清单14.4 Circle.cpp

#include <iostream>

constexpr double getPi()
{
    return (double)22 / 7; //获取Π的近似值
}

int main()
{
    float radius;
    std::cout << "Enter the radius of the circle: ";
    std::cin >> radius;
    double area = getPi() * (radius * radius);
    std::cout << "\nCircle's area: " << area << std::endl;
    return 0;
}

第十五章 运算符重载

15.1重载运算符

对于c++内置类型,对其使用相应运算符,编译器能准确知道其意思,比如:

int x = 17,y = 12,z;
z = x * (y + 5);

通过使用成员函数multiply()和add(),类也能提供这样的功能,但语法复杂得多。假如有个表示整数的Number类,下述代码与上例的相同:

Number x(17);
Number y(12);
Number z,temp;
temp = y.add(5);
z = x.multiply(temp);

这些代码将5与y相加,再将结果与x相乘,最终结果同样是289

为了简化代码,可重载运算符,这样便可使用运算符来操作对象。

运算符重载定义了将运算符用于对象时执行的操作,几乎所有的c++运算符都可重载。

程序清单15.1 Counter.cpp

#include<iostream>

class Counter
{
private:
    int value;
public:
    Counter();
    ~Counter(){}
    int getValue() const{return value;}
    void setValue(int x){value = x;}
};

Counter::Counter():value(0)
{}

int main()
{
    Counter c;
    std::cout<<"The value of c is "<<c.getValue()<<std::endl;
    return 0;
}

运行结果就不贴了。这个程序并未进行运算符重载,也没有进行改动,Counter对象也不能进行递增、递减、相加和赋值,不能用其他运算符操作它,显示其值也不容易(需要调用成员函数进行显示)。

编写递增方法

通过重载运算符,可给类提供原本不能进行的运算符操作。

要在类中重载运算符,最常见的方式是使用成员函数。

函数名由operator和要定义的运算符(如+或++)组成

程序清单15.2 Counter2.cpp

#include <iostream>

class Counter
{
private:
    int value;

public:
    Counter();
    ~Counter() {}
    int getValue() const { return value; }
    void setValue(int x) { value = x; }
    void increment() { ++value; }
    const Counter &operator++(); //重载函数声明
};

Counter::Counter() : value(0) {}

const Counter &Counter::operator++()
{
    ++value;
    return *this;//对this指针解引用以返回当前对象
}

int main()
{
    Counter c;
    std::cout << "The value of c is " << c.getValue() << std::endl;
    c.increment();
    std::cout << "The value of c is " << c.getValue() << std::endl;
    ++c;//++重载
    std::cout << "The value of c is " << c.getValue() << std::endl;
    Counter a = ++c;
    std::cout << "The value of a: " << a.getValue() << std::endl;
    std::cout << " and c: " << c.getValue() << std::endl;
}

重载后缀运算符

给成员函数operator++()添加一个int参数。在函数体内,不会使用这个参数,它只用于表明改函数定义的是后缀运算符。

为此,在重载的成员函数中,必须创建一个临时对象,用于存储原始值,以便对原对象进行递增。返回的将是原始对象,因为后缀运算符要求使用原始值,而不是递增后的值。

必须按值(而不是按引用)返回该临时对象,否则函数返回时它将不再在作用域内。

程序清单15.3 Counter3.cpp

#include <iostream>

class Counter
{
private:
    int value;

public:
    Counter();
    ~Counter() {}
    int getValue() const { return value; }
    void setValue(int x) { value = x; }
    const Counter &operator++();
    const Counter operator++(int);
};

Counter::Counter() : value(0) {}

const Counter &Counter::operator++()
{
    ++value;
    return *this;
}

const Counter Counter::operator++(int)
{
    Counter temp(*this);
    ++value;
    return temp;
}

int main()
{
    Counter c;
    std::cout << "The value of c is " << c.getValue() << std::endl;
    c++;
    std::cout << "The value of c is " << c.getValue() << std::endl;
    ++c;
    std::cout << "The value of c is " << c.getValue() << std::endl;
    Counter a = ++c;
    std::cout << "The value of a : " << a.getValue() << std::endl;
    std::cout << "and c : " << c.getValue() << std::endl;
    a = c++;
    std::cout << "The value of a : " << a.getValue() << std::endl;
    std::cout << "and c : " << c.getValue() << std::endl;
    return 0;
}

重载加法运算符

和上面的意识是大同小异

程序清单15.4 Counter4.cpp

#include <iostream>

class Counter
{
private:
    int value;

public:
    Counter();
    Counter(int intialValue);
    ~Counter() {}
    int getValue() const { return value; }
    void setValue(int x) { value = x; }
    Counter operator+(const Counter &);
};

Counter::Counter() : value(0) {}
Counter::Counter(int intialValue) : value(intialValue) {}
Counter Counter::operator+(const Counter &rhs)//重载+运算符
{
    return Counter(value + rhs.getValue());
}

int main()
{
    Counter alpha(4), beta(13), gamma;
    gamma = alpha + beta;
    std::cout << "alpha: " << alpha.getValue() << std::endl;
    std::cout << "beta: " << beta.getValue() << std::endl;
    std::cout << "gamma: " << gamma.getValue() << std::endl;
    return 0;
}

对运算符重载的限制

不能重载用于内置类型的运算符:不能改变运算符的优先级和目数(单目、双目或三目);另外,不能创建新运算符,因此不能将**声明为指数(乘方)运算符。

运算符重载是c++新手过度使用和滥用的c++功能之一,他们经常禁不住诱惑,给一些晦涩的运算符提供有趣的新用途,但这常常会导致代码令人迷惑,难以理解。

赋值运算符

赋值运算符的重载函数为operator=(),重载后每当给对象赋值都将调用它。

重载赋值运算符时,有如下问题需要考虑:

  • 如果将一个对象赋值给另一个对象,比如dallas = wichita;这时对象dallas之前的内存如果是在堆中分配的,那么就应该考虑重载=运算符时释放该对象所指向的堆内存后再进行赋值。
  • 如果将一个对象赋值给自己,比如dallas = dallas;,这种情况可能意外发生。再结合上面的释放内存问题,这个时候就可能导致dallas将堆中分配给自己的内存释放,这样,就会出现无法预料的意外。为了避免这种问题,可以使用this指针检查右操作数是否为当前对象。

程序清单15.5 Assignment.cpp

#include <iostream>

class Tricycle
{
private:
    int *speed;

public:
    Tricycle();
    ~Tricycle();
    int getSpeed() const { return *speed; }
    void setSpeed(int newSpeed) { *speed = newSpeed; }
    Tricycle operator=(const Tricycle &);
};

Tricycle::Tricycle()
{
    speed = new int;
    *speed = 5;
}

Tricycle::~Tricycle()
{
    delete speed;
}

Tricycle Tricycle::operator=(const Tricycle &rhs)
{
    if (this == &rhs)
        return *this;
    delete speed;
    speed = new int;
    *speed = rhs.getSpeed();
    return *this;
}

int main()
{
    Tricycle wichita;
    std::cout << "Wichita's speed: " << wichita.getSpeed() << std::endl;
    std::cout << "Setting Wichita's speed to 6 ..." << std::endl;
    wichita.setSpeed(6);
    Tricycle dallas;
    std::cout << "Dallas's speed: " << dallas.getSpeed() << std::endl;
    std::cout << "Copying Wichita to Dallas ..." << std::endl;
    wichita = dallas;
    std::cout << "Dallas's speed: " << dallas.getSpeed() << std::endl;
    return 0;
}

剩下的那些运算符也都大同小异,只要重载运算符时考虑那些可能会出现bug的细节。

15.2转换运算符

如果您视图将一个内置类型赋值给一个用户自定义的类型,如果不创建转换运算符而直接赋值会导致编译失败,比如:

int beta = 5;
Counter alpha = beta;//报错,无法将int转换为Counter对象

下面这个程序通过创建一个转换运算符(其实是一个构造函数,接受一个int参数并创建一个Counter对象)修复了这个问题。

程序清单15.7 Counter6.cpp

#include <iostream>

class Counter
{
private:
    int value;

public:
    Counter() : value(0) {}
    Counter(int newValue);
    ~Counter() {}
    int getValue() const { return value; }
    void setValue(int newValue) { value = newValue; }
};

Counter::Counter(int newValue) : value(newValue) {}

int main()
{
    int beta = 5;
    Counter alpha = beta;
    std::cout << "alpha: " << alpha.getValue() << std::endl;
    return 0;
}

运行输出是:alpha:5

上面是将内置类型变量赋值给对象,下面将演示将对象赋值给内置变量类型。

c++支持在类中添加转换运算符,以指定如何将对象隐式地转换为内置类型变量。

程序清单15,8 Counter7.cpp

#include <iostream>

class Counter
{
private:
    int value;

public:
    Counter() : value(0) {}
    Counter(int newValue);
    ~Counter() {}
    int getValue() const { return value; }
    void setValue(int newValue) { value = newValue; }
    operator unsigned int();
};

Counter::Counter(int newValue) : value(newValue) {}

Counter::operator unsigned int() //转换运算符
{
    return (value);
}

int main()
{
    Counter epsilon(19);
    int zeta = epsilon;
    std::cout << "zeta: " << zeta << std::endl;
    return 0;
}

运行输出是:zeta: 19

注意转换运算符没有指定返回值,但实际上返回了一个转换后的值。

第十六章 使用继承扩展类

16.1什么是继承

如果一个类在现有类的基础上添加了新功能,那么这个类就被称为从原来的类派生而来的派生类(子类),而原来的类称为新类的基类(父类)。

基类可以有多个派生类

在c++中,要从一个类派生出另一个类,可在类声明中的类名后加上冒号,再指定类的访问控制符(public、protected或private)以及基类。

程序清单16.1 Mammal1.cpp

#include <iostream>

enum BREED
{
    YORKIE,
    CAIRN,
    DANDIE,
    SHETLAND,
    DOBERMAN,
    LAB
};

class Mammal
{
protected:
    int age;
    int weight;

public:
    Mammal();
    ~Mammal();
    int getAge() const;
    void setAge(int);
    int getWeight() const;
    void setWeight();
    void speak();
    void sleep();
};

class Dog : public Mammal
{
protected:
    BREED itsBreed;

public:
    Dog();
    ~Dog();

    BREED getBreed() const;
    void setBreed(BREED);
    void wagTail();
    void begForBreed();
};

int main()
{
    return 0;
}

这个程序能够通过编译,但是没有任何输出。它只包含类声明,而没有实现。

​ 若希望数据对当前类和它的派生类可见,为此可使用protected。受保护的数据成员和函数对派生类来说是可见的,但其他方面与私有成员完全相同。

​ 有三个访问限定符:public、protected和private。只要有类的对象,其成员函数就能够访问该类的所有成员数据和成员函数。成员函数可访问基类的所有私有数据和函数。

16.2 私有和保护

程序清单16.2 Mammal2.cpp

#include <iostream>

enum BREED
{
    YORKIE,
    CAIRN,
    DANDIE,
    SHETLAND,
    DOBERMAN,
    LAB
};

class Mammal
{
protected:
    int age;
    int weight;

public:
    Mammal() : age(2), weight(5) {}
    ~Mammal() {}
    int getAge() const { return age; }
    void setAge(int newAge) { age = newAge; }
    int getWeight() const { return weight; }
    void setWeight(int newWeight) { weight = newWeight; }
    void speak() const { std::cout << "Mammal sound!\n"; }
    void sleep() const { std::cout << "Shhh. I'm sleeping.\n"; }
};

class Dog : public Mammal
{
private:
    BREED breed;

public:
    Dog() : breed(YORKIE) {}
    ~Dog() {}

    BREED getBreed() const { return breed; }
    void setBreed(BREED newBreed) { breed = newBreed; }
    void wagTail() { std::cout << "Tail wagging ...\n"; }
    void begForBreed() { std::cout << "Begging for food ...\n"; }
};

int main()
{
    Dog fido;
    fido.speak();
    fido.wagTail();
    std::cout << "Fido is " << fido.getAge() << " years oid\n";
    return 0;
}

16.3构造函数和析构函数

创建派生类对象时,将调用多个构造函数。

所以创建一个子类对象时,将先调用基类的构造函数,再调用子类的构造函数;销毁对象时,将先调用子类的析构函数,再调用基类的析构函数。

程序清单16.3 Mammal3.cpp

#include <iostream>

enum BREED
{
    YORKIE,
    CAIRN,
    DANDIE,
    SHETLAND,
    DOBERMAN,
    LAB
};

class Mammal
{
protected:
    int age;
    int weight;

public:
    Mammal() : age(2), weight(5) { std::cout << "Mammal constructor ..."; }
    ~Mammal() { std::cout << "Mammal destructor ..."; }
    int getAge() const { return age; }
    void setAge(int newAge) { age = newAge; }
    int getWeight() const { return weight; }
    void setWeight(int newWeight) { weight = newWeight; }
    void speak() const { std::cout << "Mammal sound!\n"; }
    void sleep() const { std::cout << "Shhh. I'm sleeping.\n"; }
};

class Dog : public Mammal
{
private:
    BREED breed;

public:
    Dog() : breed(YORKIE) { std::cout << "Dog constructor ..."; }
    ~Dog() { std::cout << "Dog destructor ..."; }

    BREED getBreed() const { return breed; }
    void setBreed(BREED newBreed) { breed = newBreed; }
    void wagTail() { std::cout << "Tail wagging ...\n"; }
    void begForBreed() { std::cout << "Begging for food ...\n"; }
};

int main()
{
    Dog fido;
    fido.speak();
    fido.wagTail();
    std::cout << "Fido is " << fido.getAge() << " years oid\n";
    return 0;
}

16.4将参数传递给基类构造函数

要在派生类的初始化阶段进行基类初始化,可指定基类名称,并在后面跟基类构造函数需要的参数。

程序清单16.4 Mammal4.cpp

#include <iostream>

enum BREED
{
    YORKIE,
    CAIRN,
    DANDIE,
    SHETLAND,
    DOBERMAN,
    LAB
};

class Mammal
{
protected:
    int age;
    int weight;

public:
    Mammal() : age(1), weight(5) { std::cout << "Mammal constructor ...\n"; }
    Mammal(int age) : age(age), weight(5) { std::cout << "Mammal(int) constructor ...\n"; }
    ~Mammal() { std::cout << "Mammal destructor ...\n"; }
    int getAge() const { return age; }
    void setAge(int newAge) { age = newAge; }
    int getWeight() const { return weight; }
    void setWeight(int newWeight) { weight = newWeight; }
    void speak() const { std::cout << "Mammal sound!\n"; }
    void sleep() const { std::cout << "Shhh. I'm sleeping.\n"; }
};

class Dog : public Mammal
{
private:
    BREED breed;

public:
    Dog() : Mammal(), breed(YORKIE) { std::cout << "Dog constructor ...\n"; }
    Dog(int age) : Mammal(age), breed(YORKIE) { std::cout << "Dog(int) constructor ...\n"; }
    Dog(int age, int newWeight) : Mammal(age), breed(YORKIE)
    {
        weight = newWeight;
        std::cout << "Dog(int,int) constructor ...\n";
    }
    Dog(int age, int newWeight, BREED breed) : Mammal(age), breed(breed)
    {
        weight = newWeight;
        std::cout << "Dog(int,int,BREED) constructor ...\n";
    }
    Dog(int age, BREED newBreed) : Mammal(age), breed(newBreed) { std::cout << "Dog(int,BREED) constructor ...\n"; }
    ~Dog() { std::cout << "Dog destructor ...\n"; }

    BREED getBreed() const { return breed; }
    void setBreed(BREED newBreed) { breed = newBreed; }
    void wagTail() { std::cout << "Tail wagging ...\n"; }
    void begForBreed() { std::cout << "Begging for food ...\n"; }
};

int main()
{
    Dog fido;
    Dog rover(5);
    Dog buster(6, 8);
    Dog yorkie(3, YORKIE);
    Dog dobbie(4, 20, DOBERMAN);
    fido.speak();
    rover.wagTail();
    std::cout << "Yorkie is " << yorkie.getAge() << " years old\n";
    std::cout << "Dobbie weights: " << dobbie.getWeight() << " pounds\n";
    return 0;
}

16.5重写函数

如果派生类创建了一个返回类型和签名都与基类成员函数相同的函数,但是提供了新的实现,就称之为重写(覆盖)该函数。

重写函数时,返回类型和签名必须与基类函数相同。签名指的是除返回类型外的函数原型,这包括函数名、参数列表及关键字const(如果被重写的函数使用了const)

函数的签名由其名称以及参数的数量和类型组成,但不包括返回类型。

程序清单16.5 Mammal5.cpp

#include <iostream>

enum BREED
{
    YORKIE,
    CAIRN,
    DANDIE,
    SHETLAND,
    DOBERMAN,
    LAB
};

class Mammal
{
protected:
    int age;
    int weight;

public:
    Mammal() : age(2), weight(5) { std::cout << "Mammal constructor ...\n"; }
    ~Mammal() { std::cout << "Mammal destructor ...\n"; }

    void speak() const { std::cout << "Mammal sound!\n"; }
    void sleep() const { std::cout << "Shhh. I'm sleeping.\n"; }
};

class Dog : public Mammal
{
private:
    BREED breed;

public:
    Dog() : breed(YORKIE) { std::cout << "Dog constructor ...\n"; }
    ~Dog() { std::cout << "Dog destructor ...\n"; }

    void wagTail() { std::cout << "Tail wagging ...\n"; }
    void begForBreed() { std::cout << "Begging for food ...\n"; }
    void speak() const { std::cout << "Woof!\n"; }
};

int main()
{
    Mammal mammal;
    Dog dog;
    mammal.speak();
    dog.speak();
    return 0;
}

重写与重载不同,重载成员函数实际上是创建了多个名称相同但签名不同的函数。但重写成员函数时,在派生类中创建的是一个名称与签名都与基类函数相同的函数。

如果Mammal有三个move()的重载版本,一个不接受任何参数,一个接受一个int型参数,一个接受int型和其他参数,而Dog只重写了无参版本的函数,那么使用Dog对象将难以访问其他两个版本。

程序清单16.6 Mammal6.cpp

#include <iostream>

class Mammal
{
protected:
    int age;
    int weight;

public:
    void move() const { std::cout << "Mammal moves one step\n"; }
    void move(int distance) const { std::cout << "Mammal moves " << distance << " steps\n"; }
};

class Dog : public Mammal
{
public:
    void move() const { std::cout << "Dog moves 5 steps\n"; }
};

int main()
{
    Mammal mammal;
    Dog dog;
    mammal.move();
    mammal.move(2);
    dog.move();
    //dog.move(10);//报错
    return 0;
}

即便重写了基类的成员函数,仍可使用全限定名来调用它。为此,可指定基类名、冒号和函数名

程序清单16.7 Mammal7.cpp

#include <iostream>

class Mammal
{
protected:
    int age;
    int weight;

public:
    void move() const { std::cout << "Mammal moves one step\n"; }
    void move(int distance) const { std::cout << "Mammal moves " << distance << " steps\n"; }
};

class Dog : public Mammal
{
public:
    void move() const
    {
        std::cout << "Dog moves ...\n";
        Mammal::move(3);
    }
};

int main()
{
    Mammal mammal;
    Dog dog;
    mammal.move(2);
    dog.move();
    dog.Mammal::move(6); //显式调用
    return 0;
}

第十七章 使用多态和派生类

17.1使用虚成员函数实现多态

为实现多态,使用Mammal指针来调用成员函数,不用知道也不用关心该指针指向的是哪种对象以及该对象的成员函数是如何实现的。

要声明虚成员函数,可使用关键字virtual

程序清单17.1 Mammal8.cpp

#include <iostream>

class Mammal
{
private:
    int age;

public:
    Mammal() : age(1) { std::cout << "Mammal constructor ...\n"; }
    ~Mammal() { std::cout << "Mammal destructor ...\n"; }
    void move() const { std::cout << "Mammal, move one step\n"; }
    virtual void sepak() const { std::cout << "Mammal speak!\n"; }
};

class Dog : public Mammal
{
public:
    Dog() { std::cout << "Dog constructor ...\n"; }
    ~Dog() { std::cout << "Dog destructor ...\n"; }
    void move() const { std::cout << "Dog move 5 steps ...\n"; }
    void sepak() const { std::cout << "Woof!\n"; }
    void wagTail() { std::cout << "Wagging tail ...\n"; }
};

int main()
{
    Mammal *pDog = new Dog;
    pDog->move();
    pDog->sepak();
    return 0;
}

image-20200723134735386

你要观察到创建的是父类的指针,但是却将一个子类对象的地址赋值给它,所以这是父类指针指向子类对象。所以按照子类对象的构造函数顺序是先调用父类构造,然后调用子类构造。因为编译器知道这是一个父类指针,所以未使用virtual关键字的函数都先在父类中去找。如果是调用虚成员函数,编译器则会去子类中找。

程序清单17.2 Mammal9.cpp

#include <iostream>

class Mammal
{
protected:
    int age;

public:
    Mammal() : age(1) {}
    ~Mammal() {}
    virtual void sepak() const { std::cout << "Mammal speak!\n"; }
};

class Dog : public Mammal
{
public:
    void sepak() const { std::cout << "Woof!\n"; }
};

class Cat : public Mammal
{
public:
    void sepak() const { std::cout << "Meow!\n"; }
};

class Horse : public Mammal
{
public:
    void sepak() const { std::cout << "Whinny!\n"; }
};

class Pig : public Mammal
{
public:
    void sepak() const { std::cout << "Oink!\n"; }
};

int main()
{
    Mammal *array[5];
    Mammal *ptr;
    int choice, i;
    for (i = 0; i < 5; i++)
    {
        std::cout << "(1) dog (2) cat (3) horse (4) pig: ";
        std::cin >> choice;
        switch (choice)
        {
        case 1:
            ptr = new Dog;
            break;
        case 2:
            ptr = new Cat;
            break;
        case 3:
            ptr = new Horse;
            break;
        case 4:
            ptr = new Pig;
            break;
        default:
            ptr = new Mammal;
            break;
        }
        array[i] = ptr;
    }
    for (i = 0; i < 5; i++)
    {
        array[i]->sepak();
    }
    for (i = 0; i < 5; i++)
    {
        delete array[i];
    }
    return 0;
}

image-20200723141757197

17.2虚成员函数的工作原理

创建派生对象时,首先调用基类(父类)的构造函数,然后调用派生类(子类)的构造函数。说明整个派生对象包括了父类构造函数与子类构造函数所创造对象两部分在内存中是相邻的。

在类中创建虚成员函数之后,这个类的对象必须跟踪它。很多编译器会创建虚成员函数表(v-table),每个类都有一个虚成员函数表,而每个对象都有一个指向虚成员函数表的指针(vptr或v-pointer)

创建Dog的Mammal部分时,vptr被初始化为指向Mammal的虚成员函数。

调用Dog的构造函数以添加对象的Dog部分时,将调整vptr指针,使其指向Dog类重写的虚成员函数。

使用Mammal指针时,vptr将根据Mammal指针所指向对象的实际类型指向正确的函数。

子类有而父类没有的函数不能通过父类指针来进行访问,除非将其强制转换为子类指针。

仅当通过指针和引用进行调用时,才能发挥成员函数的魔力。

程序清单17.3 Mammal10.cpp

#include <iostream>

class Mammal
{
protected:
    int age;

public:
    Mammal() : age(1) {}
    ~Mammal() {}
    virtual void sepak() const { std::cout << "Mammal speak!\n"; }
};

class Dog : public Mammal
{
public:
    void sepak() const { std::cout << "Woof!\n"; }
};

class Cat : public Mammal
{
public:
    void sepak() const { std::cout << "Meow!\n"; }
};

void valueFunction(Mammal);
void ptrFunction(Mammal *);
void refFunction(Mammal &);

int main()
{
    Mammal *ptr = 0;
    int choice;
    while (1)
    {
        bool fQuit = false;
        std::cout << "(1) dog (2) cat (0) quit: ";
        std::cin >> choice;
        switch (choice)
        {
        case 0:
            fQuit = true;
            break;
        case 1:
            ptr = new Dog;
            break;
        case 2:
            ptr = new Cat;
            break;
        default:
            ptr = new Mammal;
            break;
        }
        if (fQuit)
        {
            break;
        }
        ptrFunction(ptr);
        refFunction(*ptr);
        valueFunction(*ptr);
    }
    return 0;
}

void valueFunction(Mammal mammalValue) { mammalValue.sepak(); }
void ptrFunction(Mammal *pMammal) { pMammal->sepak(); }
void refFunction(Mammal &rMammal) { rMammal.sepak(); }

image-20200723145028263

虚析构函数:

在c++中,在需要基类指针的地方使用指向派生对象的指针是一种合法且常见的做法。

当指向派生对象的指针被删除时将发生什么情况呢?如果析构函数是虚成员函数,将执行正确的操作:调用派生类的析构函数。由于派生类的析构函数会自动地调用基类的析构函数,因此整个对象将被正确地销毁。

经验规则是:如果类中的任何一个函数是虚成员函数,那么析构函数也应该是虚成员函数。

虚复制构造函数

构造函数不能是虚成员函数,然而,有时候程序非常需要通过传递一个指向基类对象的指针,创建派生类对象的拷贝。对于这种问题,一种常见的解决方法是,在基类中创建一个clone()成员函数,并将其设置为虚成员函数。clone()函数创建当前对象的拷贝,并返回该对象。

由于每个派生类对象都重写函数clone(),因此使其创建派生类对象的拷贝。

程序清单17.4 Mammal11.cpp