一、你好世界
选择 IDE
要开始用 C++ 开发,你需要一个文本编辑器和一个 C++ 编译器。你可以通过安装一个支持 C++ 的集成开发环境(IDE)来同时获得这两者。一个很好的选择是微软的 Visual Studio 社区版,这是一个免费的 Visual Studio 版本,可以从微软的网站上获得。 1 这款 IDE 内置了对 C++11 标准的支持,也包含了 2015 版 C++14 的许多特性。
另外两个流行的跨平台 ide 包括 NetBeans 和 Eclipse CDT。或者,您可以使用简单的文本编辑器(如记事本)进行开发,尽管这不如使用 IDE 方便。如果您选择这样做,只需创建一个文件扩展名为. cpp 的空文档,并在您选择的编辑器中打开它。
创建项目
安装 Visual Studio 后,继续启动程序。然后您需要创建一个项目,它将管理 C++ 源文件和其他资源。转到 Visual Studio 中的文件新建
项目以显示新建项目窗口。从这里,在左侧框架中选择 Visual C++ 模板类型。然后在右侧框架中选择 Win32 控制台应用程序模板。在窗口底部,您可以配置项目的名称和位置。完成后,单击“确定”按钮,将出现另一个名为 Win32 应用程序向导的对话框。单击下一步,将显示几个应用程序设置。将应用程序类型保留为控制台应用程序,并选中空项目复选框。然后单击 Finish,让向导创建您的空项目。
添加源文件
您现在已经创建了一个 C++ 项目。在解决方案资源管理器窗格(视图解决方案资源管理器)中,您可以看到该项目由三个空文件夹组成:头文件、资源文件和源文件。右键单击源文件文件夹并选择添加
新项目。从“添加新项”对话框中选择 C++ 文件(。cpp)模板。
将这个源文件命名为“MyApp ”,然后单击 Add 按钮。现在,一个空的 cpp 文件将被添加到您的项目中,并为您打开。
你好世界
首先要添加到源文件中的是 main 函数。这是程序的入口点,花括号内的代码是程序运行时将要执行的代码。括号及其内容被称为代码块,或简称为代码块。
int main() {}
第一个应用程序将简单地向屏幕输出文本“Hello World”。在此之前,需要包含 iostream 头。这个头文件为程序提供了输入和输出功能,是所有 C++ 编译器附带的标准库文件之一。#include
指令的作用是在文件被编译成可执行文件之前,用指定文件头中的所有内容替换该行。
#include <iostream>
int main() {}
通过包含的 iostream,您可以使用几个新功能。这些都位于名为std
的标准名称空间中,您可以使用双冒号来检查它,也称为范围解析操作符(::
)。在 Visual Studio 中键入此内容后,IntelliSense 窗口将自动打开,显示命名空间包含的内容。在这些成员中,您可以找到cout
流,这是 C++ 中的标准输出流,将用于将文本打印到控制台窗口。它使用两个小于号作为插入操作符(<<
)来表示输出什么。然后可以指定字符串,用双引号分隔,后跟分号。分号在 C++ 中用来标记所有语句的结束。
#include <iostream>
int main()
{
std::cout << "Hello World";
}
使用名称空间
为了简单一点,您可以添加一行代码,指定代码文件使用标准名称空间。然后,您不再需要在名称空间(std::
)前面加上前缀cout
,因为它现在是默认使用的。
#include <iostream>
using namespace std;
int main()
{
cout << "Hello World";
}
智能感知
在 Visual Studio 中编写代码时,只要有多个预先确定的选项可供选择,就会弹出一个名为 IntelliSense 的窗口。也可以通过按 Ctrl+Space 随时手动打开该窗口,以便快速访问您可以在程序中使用的任何代码实体。这是一个非常强大的功能,你应该学会好好利用。
1
二、编译并运行
Visual Studio 编译
继续上一章,Hello World 程序现在已经完成,可以编译和运行了。您可以通过转到“调试”菜单并单击“启动而不调试”(Ctrl + F5)来完成此操作。然后,Visual Studio 编译并运行在控制台窗口中显示文本的应用程序。
如果您从“调试”菜单中选择“开始调试”( F5 ),显示 Hello World 的控制台窗口将在主函数完成后立即关闭。为了防止这种情况,你可以在 main 的末尾添加一个对cin::get
函数 的调用。该函数属于控制台输入流,将从键盘读取输入,直到按下 return 键。
#include <iostream>
using namespace std;
int main()
{
cout << "Hello World";
cin.get();
}
控制台编译
作为使用 IDE 的替代方法,只要您有 C++ 编译器,您也可以从终端窗口编译源文件。例如,在一台 Linux 机器上,你可以使用 GNU C++ 编译器,它可以在几乎所有的 Unix 系统上使用,包括 Linux 和 BSD 家族,作为 GNU 编译器集合(GCC)的一部分。这个编译器也可以通过下载 MinGW 安装在 Windows 上,或者作为 Xcode 开发环境的一部分安装在 Mac 上。
要使用 GNU 编译器,你需要在终端窗口中输入它的名字“g++ ”,并给它输入和输出文件名作为参数。然后,它生成一个可执行文件,该文件在运行时产生的结果与在 Visual Studio 的 Windows 下编译的结果相同。
g++ MyApp.cpp -o MyApp.exe
./MyApp.exe
Hello World
评论
注释用于在源代码中插入注释。它们对最终程序没有影响,只是为了增强代码的可读性,对您和其他开发人员都是如此。C++ 有两种注释符号——单行和多行。单行注释以//开始,延伸到行尾。
*//* single-line comment
多行注释可以跨越多行,并用/和/分隔。
*/** multi-line comment **/*
请记住,空白字符——比如注释、空格和制表符——通常会被编译器忽略。这让你在如何格式化你的代码上有很大的自由度。
1
三、变量
变量用于在程序执行期间存储数据。
数据类型
根据您需要存储的数据,有几种内置数据类型。这些通常被称为基本数据类型或原语。整数类型有short
、int
、long
和long long
。float
、double
和long double
类型是浮点(实数)类型。char
类型保存单个字符,而bool
类型包含 true 或 false 值。
|
数据类型
|
大小(字节)
|
描述
|
| — | — | — |
| 茶 | one | 整数或字符 |
| 短的 | Two | |
| (同 Internationalorganizations)国际组织 | four | 整数 |
| 长的 | 4 或 8 | |
| 很长很长 | eight | |
| 漂浮物 | four | |
| 两倍 | eight | 浮点数 |
| 长双 | 8 或 16 岁 | |
| 弯曲件 | one | 布尔值 |
在 C++ 中,数据类型的确切大小和范围是不固定的。相反,它们依赖于编译程序的系统。上表中显示的大小是大多数 32 位系统上的大小,以 C++ 字节为单位。C++ 中的一个字节是内存的最小可寻址单元,保证至少为 8 位,但也可能是 16 或 32 位,具体取决于系统。根据定义,C++ 中的字符大小是 1 字节。此外,int 类型的大小与处理器的字长相同,因此对于 32 位系统,整数的大小是 32 位。表中的每个整数类型也必须至少与它前面的整数类型一样大。这同样适用于浮点类型,其中每个类型必须至少提供与前一个类型一样的精度。
声明变量
为了声明(创建)一个变量,你从你希望变量保存的数据类型开始,后面跟着一个标识符,这是变量的名字。名称可以由字母、数字和下划线组成,但不能以数字开头。它也不能包含空格或特殊字符,并且不能是保留关键字。
int myInt; // correct int _myInt32; // correct
int 32Int; // incorrect (starts with number)
int Int 32; // incorrect (contains space)
int Int@32; // incorrect (contains special character)
int new; // incorrect (reserved keyword)
分配变量
为了给一个声明的变量赋值,使用等号,它被称为赋值操作符 ( =
)。
myInt = 50;
声明和赋值可以合并成一条语句。当一个变量被赋值时,它就变成了定义的。
int myInt = 50;
在变量被声明的同时,有一种替代的方法来赋值,或者初始化,用圆括号将值括起来。这就是所谓的构造器初始化 ,相当于上面的语句。
int myAlt (50);
如果你需要创建一个以上的相同类型的变量,有一个简单的方法可以使用逗号操作符(,
)。
int x = 1, y = 2, z;
一旦变量被定义(声明和赋值),你就可以通过引用变量的名字来使用它:例如,打印它。
std::cout << x << y; // "12"
变量作用域
变量的作用域指的是可以使用该变量的代码区域。C++ 中的变量既可以全局声明,也可以局部声明。全局变量是在任何代码块之外声明的,声明之后可以从任何地方访问。另一方面,局部变量是在函数内部声明的,只有在声明后才能在函数内部访问。局部变量的生存期也是有限的。一个全局变量将在程序运行期间保持分配状态,而一个局部变量将在其函数执行完毕后被销毁。
int globalVar; // global variable
int main() { int localVar; } // local variable
这些变量的默认值也不同。编译器会自动将全局变量初始化为零,而局部变量根本不会初始化。因此,未初始化的局部变量将包含该内存位置中已经存在的任何垃圾。
int globalVar; // initialized to 0
int main()
{
int localVar; // uninitialized
}
使用未初始化的变量是一个常见的编程错误,可能会产生意想不到的结果。因此,在声明局部变量时,最好总是给它们一个初始值。
int main()
{
int localVar = 0; // initialized to 0
}
整数类型
有四种整数类型可以使用,这取决于你需要变量保存多大的数。
char myChar = 0; // -128 to +127
short myShort = 0; // -32768 to +32767
int myInt = 0; // -2³¹ to +2³¹-1
long myLong = 0; // -2³¹ to +2³¹-1
C++11 标准化了第五种整数类型 long long,它保证至少有 64 位大。许多编译器早在 C++11 标准完成之前就开始支持这种数据类型,包括 Microsoft C++ 编译器。
long long myL2 = 0; // -2⁶³ to +2⁶³-1
要确定数据类型的确切大小,可以使用sizeof
操作符。该运算符返回数据类型在您正在编译的系统中所占的字节数。
std::cout << sizeof(myChar) // 1 byte (per definition)
<< sizeof(myShort) // 2
<< sizeof(myInt) // 4
<< sizeof(myLong) // 4
<< sizeof(myL2); // 8
C++11 中增加了固定大小的整数类型。这些类型属于 std 命名空间,可以通过 cstdint 标准库头包含。
#include <cstdint>
using namespace std;
int8_t myInt8 = 0; // 8 bits
int16_t myInt16 = 0; // 16 bits
int32_t myInt32 = 0; // 32 bits
int64_t myInt64 = 0; // 64 bits
有符号和无符号整数
默认情况下,Microsoft C++ 中的所有数字类型都是有符号的,因此可能同时包含正值和负值。要显式声明一个变量为有符号变量,可以使用关键字signed
。
signed char myChar = 0; // -128 to +127
signed short myShort = 0; // -32768 to +32767
signed int myInt = 0; // -2³¹ to +2³¹-1
signed long myLong = 0; // -2³¹ to +2³¹-1
signed long long myL2= 0; // -2⁶³ to +2⁶³-1
如果你只需要存储正值,你可以声明整数类型为 unsigned
来加倍它们的上限。
unsigned char myChar = 0; // 0 to 255
unsigned short myShort = 0; // 0 to 65535
unsigned int myInt = 0; // 0 to 2³²-1
unsigned long myLong = 0; // 0 to 2³²-1
unsigned long long myL2= 0; // 0 to 2⁶⁴-1
signed
和unsigned
关键字可以作为独立类型使用,是signed int
和unsigned int
的简称。
unsigned uInt; // unsigned int
signed sInt; // signed int
同样,short
和long
数据类型是short int
和long int
的缩写。
short myShort; // short int
long myLong; // long int
数字文字
除了标准的十进制记数法,整数也可以用八进制或十六进制记数法来赋值。八进制文本使用前缀“0”,十六进制文本以“0x”开头下面的两个数字代表同一个数,在十进制记数法中是 50。
int myOct = 062; // octal notation (0)
int myHex = 0x32; // hexadecimal notation (0x)
从 C++14 开始,出现了一种二进制表示法,它使用“0b”作为前缀。这个版本的标准还增加了一个数字分隔符('),可以更容易地阅读长数字。下面的二进制数代表十进制数中的 50。
int myBin = 0b0011'0010; // binary notation (0b)
浮点类型
浮点类型可以存储不同精度级别的实数。
float myFloat; // ~7 digits
double myDouble; // ~15 digits
long double myLongDouble; // typically same as double
上面显示的精度是指数字的总位数。浮点型可以精确地表示 7 位数,而双精度型可以处理 15 位数。试图给一个float
分配 7 个以上的数字意味着最低有效数字将被四舍五入。
myFloat = 12345.678; // rounded to 12345.68
浮点数和双精度数可以用十进制或指数记数法来赋值。指数(科学)记数法是在十进制指数后加上 E 或 E。
myFloat = 3e2; // 3*10² = 300
文字后缀
编译器通常将整数文字(常量)视为 int,或者根据需要将其视为更大的类型以适应该值。可以在字面上加上后缀来改变这种评价。对于整数,后缀可以是 U 和 L 的组合,分别表示无符号和长整型。C++11 还为 long long 类型添加了 LL 后缀。这些字母的顺序和大小写并不重要。
int i = 10;
long l = 10L;
unsigned long ul = 10UL;
除非另外指定,否则浮点文字被视为双精度型。F 或 F 后缀可用于指定文字为浮点类型。同样,L 或 L 后缀指定长双精度类型。
float f = 1.23F;
double d = 1.23;
long double ld = 1.23L;
编译器隐式地将文本转换为任何需要的类型,因此文本的这种类型区分通常是不必要的。如果在给 float 变量赋值时省略了 F 后缀,编译器可能会给出警告,因为从 double 到 float 的转换会损失精度。
字符类型
char
类型通常用于表示 ASCII 字符。这种字符常量用单引号括起来,可以存储在 char 类型的变量中。
char c = 'x'; // assigns 120 (ASCII for 'x')
存储在char
中的数字和打印char
时显示的字符之间的转换自动发生。
std::cout << c; // prints 'x'
对于要显示为字符的另一个整数类型,必须将其显式转换为char
。显式强制转换是通过将所需的数据类型放在要转换的变量或常量前面的括号中来执行的。
int i = c; // assigns 120
std::cout << i; // prints 120
std::cout << (char)i; // prints 'x'
布尔类型
bool
类型可以存储一个布尔值,这个值只能是真或假。这些值由关键字true
和false
指定。
bool b = false; // true or false value
四、运算符
数字运算符是使程序执行特定数学或逻辑操作的符号。C++ 中的数值运算符可以分为五种类型:算术、赋值、比较、逻辑和按位运算符。
算术运算符
有四个基本算术运算符,以及用于获得除法余数的模数运算符(%
)。
int x = 3 + 2; // 5 // addition
x = 3 - 2; // 1 // subtraction
x = 3 * 2; // 6 // multiplication
x = 3 / 2; // 1 // division
x = 3 % 2; // 1 // modulus (division remainder)
请注意,除法符号给出了不正确的结果。这是因为它对两个整数值进行运算,因此会截断结果并返回一个整数。要获得正确的值,必须将其中一个数字显式转换为浮点数。
float f = 3 / (float)2; // 1.5
赋值运算符
第二组是赋值操作符。最重要的是赋值操作符(=
)本身,它给变量赋值。
组合赋值运算符
赋值运算符和算术运算符的一个常见用途是对变量进行运算,然后将结果保存回同一个变量中。这些操作可以用组合赋值操作符来缩短。
x += 5; // x = x+5;
x -= 5; // x = x-5;
x *= 5; // x = x*5;
x /= 5; // x = x/5;
x %= 5; // x = x%5;
递增和递减运算符
另一个常见的操作是将变量加 1 或减 1。这可以用增量(++
)和减量(-)操作符来简化。
x++; // x = x+1;
x--; // x = x-1;
这两者都可以用在变量之前或之后。
x++; // post-increment
x--; // post-decrement
++x; // pre-increment
--x; // pre-decrement
无论使用哪个变量,变量的结果都是相同的。不同的是,后运算符在改变变量之前返回原始值,而前运算符先改变变量,然后返回值。
int x, y;
x = 5; y = x++; // y=5, x=6
x = 5; y = ++x; // y=6, x=6
比较运算符
比较运算符比较两个值并返回真或假。它们主要用于指定条件,即计算结果为 true 或 false 的表达式。
bool b = (2 == 3); // false // equal to
b = (2 != 3); // true // not equal to
b = (2 > 3); // false // greater than
b = (2 < 3); // true // less than
b = (2 >= 3); // false // greater than or equal to
b = (2 <= 3); // true // less than or equal to
逻辑运算符
逻辑运算符通常与比较运算符一起使用。如果左右两边都为真,则逻辑 and ( &&
)计算为真,如果左右两边都为真,则逻辑 or ( ||
)为真。对一个布尔结果取反,有一个逻辑非(!
)运算符。请注意,对于“逻辑与”和“逻辑或”,如果结果已经由左侧确定,则不会计算右侧。
bool b = (true && false); // false // logical and
b = (true || false); // true // logical or
b = !(true); // false // logical not
按位运算符
按位运算符可以操作整数中的单个位。例如,“按位或”运算符(|
)使结果位为 1,如果这些位设置在运算符的任一侧。
int x = 5 & 4; // 101 & 100 = 100 (4) // and
x = 5 | 4; // 101 | 100 = 101 (5) // or
x = 5 ^ 4; // 101 ^ 100 = 001 (1) // xor
x = 4 << 1; // 100 << 1 =1000 (8) // left shift
x = 4 >> 1; // 100 >> 1 = 10 (2) // right shift
x = ~4; // ~00000100 = 11111011 (-5) // invert
按位运算符也有组合赋值运算符。
int x=5; x &= 4; // 101 & 100 = 100 (4) // and
x=5; x |= 4; // 101 | 100 = 101 (5) // or
x=5; x ^= 4; // 101 ^ 100 = 001 (1) // xor
x=5; x <<= 1;// 101 << 1 =1010 (10)// left shift
x=5; x >>= 1;// 101 >> 1 = 10 (2) // right shift
运算符优先级
在 C++ 中,表达式通常从左到右计算。然而,当一个表达式包含多个操作符时,这些操作符的优先级决定了它们被求值的顺序。下表显示了优先级顺序,其中优先级最低的运算符将首先被计算。同样的基本顺序也适用于许多其他语言,如 C、Java 和 C#。
举个例子,逻辑 and ( &&
)绑定弱于关系运算符,关系运算符反过来绑定弱于算术运算符。
bool b = 2+3 > 1*4 && 5/5 == 1; // true
为了使事情更清楚,括号可以用来指定表达式的哪一部分将首先被求值。从表中可以看出,括号是优先级最低的运算符。
bool b = ((2+3) > (1*4)) && ((5/5) == 1); // true
五、指针
指针是一个变量,包含另一个变量的内存地址,称为指针对象。
创建指针 s
指针被声明为任何其他变量,除了在数据类型和指针名称之间放置一个星号(*
)。使用的数据类型决定了它将指向哪种类型的内存。
int* p; // pointer to an integer
int *q; // alternative syntax
指针可以指向相同类型的变量,方法是在该变量前面加上一个“与”号,以便检索其地址并将其分配给指针。与号被称为地址运算符(&
)。
int i = 10;
p = &i; // address of i assigned to p
取消引用指针
上面的指针现在包含了整型变量的内存地址。引用指针将检索这个地址。要获得存储在该地址中的实际值,指针必须以星号为前缀,称为解引用操作符(*
)。
std::cout << "Address of i: " << p; // ex. 0017FF1C
std::cout << "Value of i: " << *p; // 10
当写入指针时,使用相同的方法。如果没有星号,指针将被分配一个新的内存地址,如果有星号,指针所指向的变量的实际值将被更新。
p = &i; // address of i assigned to p
*p = 20; // value of i changed through p
如果创建了第二个指针并赋予了第一个指针的值,那么它将获得第一个指针的内存地址的副本。
int* p2 = p; // copy of p (copies address stored in p)
指向一个指针
有时,拥有一个可以指向另一个指针的指针会很有用。这是通过用两个星号声明一个指针,然后给它分配它将引用的指针的地址来实现的。这样,当存储在第一指针中的地址改变时,第二指针可以跟随该改变。
int** r = &p; // pointer to p (assigns address of p)
引用第二个指针现在给出了第一个指针的地址。解引用第二个指针给出变量的地址,再次解引用它给出变量的值。
std::cout << "Address of p: " << r; // ex. 0017FF28 std::cout << "Address of i: " << *r; // ex. 0017FF1C std::cout << "Value of i: " << **r; // 20
动态分配
指针的主要用途之一是在运行时分配内存——所谓的动态分配 。在迄今为止的例子中,程序只有在编译时为变量声明的那么多可用内存。这被称为静态分配。如果在运行时需要任何额外的内存,new
操作符有可以使用。该运算符允许动态分配内存,内存只能通过指针访问。new
操作符将原始数据类型或对象作为它的参数,它将返回一个指向分配的内存的指针。
int* d = new int; // dynamic allocation
关于动态分配,需要知道的一件重要事情是,当不再需要时,分配的内存不会像程序内存的其余部分一样被释放。相反,它必须用关键字delete
手动释放。这允许您控制动态分配对象的生存期,但也意味着一旦不再需要它,您就要负责删除它。忘记删除已经用new
关键字分配的内存将会给程序带来内存泄漏,因为这些内存将会一直被分配,直到程序关闭。
delete d; // release allocated memory
空指针
当指针没有被分配给有效地址时,它应该被设置为零。这样的指针叫做空指针 。这样做将允许您检查指针是否可以被安全地取消引用,因为有效的指针永远不会为零。
例如,尽管前一个指针已经释放了它的内存,但它存储的地址仍然指向一个现在不可访问的内存位置。试图取消引用这样的指针将导致运行时错误。为了帮助防止这种情况,应该将删除的指针设置为零。请注意,尝试删除已经删除的空指针是安全的。但是,如果指针没有设置为零,再次尝试删除它将导致内存损坏,并可能使程序崩溃。
delete d;
d = 0; // mark as null pointer
delete d; // safe
由于您可能不总是知道一个指针是否有效,所以每当一个指针被取消引用时都应该进行检查,以确保它不为零。
if (d != 0) { *d = 10; } // check for null pointer
常量NULL
也可以用来表示空指针。在 C++ 中,NULL
通常被定义为零,选择使用哪一个是个人喜好的问题。该常量在 stdio.h 标准库文件中定义,该文件包含在 iostream 中。
#include <iostream>
// ...
if (d != NULL) { *d = 10; } // check for null pointer
C++11 引入了关键字 nullptr 来区分 0 和空指针。使用 nullptr 的优点是,与 NULL 不同,它不会隐式转换为整数类型。文本有自己的类型 nullptr_t,它只能隐式转换为指针和 bool 类型。
int* p = nullptr; // ok
int i = nullptr; // error
bool b = nullptr; // ok (false)
nullptr_t mynull = nullptr; // ok
六、引用
引用允许程序员为变量创建一个新名字。它们为指针提供了一种更简单、更安全、功能更弱的替代方式。
创建引用
引用的声明方式与常规变量相同,只是在数据类型和变量名之间附加了一个&符号。此外,在声明引用的同时,必须用指定类型的变量对其进行初始化。
int x = 5;
int& r = x; // r is an alias to x
int &s = x; // alternative syntax
一旦引用被分配或安置,它就不能被重新安置到另一个变量。该引用实际上已成为变量的别名,可以完全像原始变量一样使用。
r = 10; // assigns value to r/x
引用和指针
引用类似于总是指向同一事物的指针。然而,指针是一个指向另一个变量的变量,而引用只是一个别名,没有自己的地址。
int* ptr = &x; // ptr assigned address to x
引用和指针指南
一般来说,只要指针不需要重新赋值,就应该使用引用,因为引用比指针更安全,因为它必须总是引用变量。这意味着不需要检查引用是否指向 null,而指针则需要这样做。引用可能是无效的——例如当引用指向空指针时——但是使用引用比使用指针更容易避免这种错误。
int* ptr = 0; // null pointer
int& ref = *ptr;
ref = 10; // segmentation fault (invalid memory access)
右值引用
C++11 带来了一种新的引用,叫做右值引用。这个引用可以绑定和修改临时对象(右值),比如文字值和函数返回值。通过在类型后放置两个&符号来形成右值引用。
int&& ref = 1 + 2; // rvalue reference
右值引用延长了临时对象的生存期,并允许像普通变量一样使用它。
ref += 3;
cout << ref; // "6"
右值引用的好处是,在处理临时对象时,可以避免不必要的复制。这提供了更好的性能,特别是在处理更大的类型时,如字符串和对象。
七、数组
数组是一种数据结构,用于存储所有具有相同数据类型的值的集合。
数组声明和分配
要声明一个数组,可以像普通的变量声明一样开始,但是在数组名后面附加一组方括号。括号包含数组中元素的数量。这些元素的默认值与变量相同——全局数组中的元素被初始化为默认值,而局部数组中的元素保持未初始化状态。
int myArray[3]; // integer array with 3 elements
数组赋值
要为元素赋值,可以通过将元素的索引放在方括号内(从零开始)来一次引用一个元素。
myArray[0] = 1;
myArray[1] = 2;
myArray[2] = 3;
或者,您可以在声明数组的同时赋值,方法是用花括号将它们括起来。指定的数组长度可以选择省略,让数组大小由赋值的数量决定。
int myArray[3] = { 1, 2, 3 };
int myArray[] = { 1, 2, 3 };
一旦数组元素被初始化,就可以通过引用所需元素的索引来访问它们。
std::cout << myArray[0]; // 1
多维数组
通过添加多组方括号,可以使数组成为多维数组。与一维数组一样,它们可以一次填充一个,也可以在声明过程中一次全部填充。
int myArray[2][2] = { { 0, 1 }, { 2, 3 } };
myArray[0][0] = 0;
myArray[0][1] = 1;
额外的花括号是可选的,但是包含它们是一个很好的实践,因为它使代码更容易理解。
int mArray[2][2] = { 0, 1, 2, 3 }; // alternative
动态数组
因为上面的数组是由静态(非动态)内存组成的,所以在执行之前必须确定它们的大小。因此,大小需要是一个常量值。为了创建一个直到运行时才知道大小的数组,你需要使用动态内存,它是用关键字new
分配的,必须分配给一个指针或引用。
int* p = new int[3]; // dynamically allocated array
C++ 中的数组表现为指向数组中第一个元素的常量指针。因此,数组元素的引用也可以用指针算法来实现。通过将指针递增 1,可以移动到数组中的下一个元素,因为对指针地址的更改会隐式地乘以指针数据类型的大小。
*(p+1) = 10; // p[1] = 10;
数组大小
就像任何其他指针一样,有可能超出数组的有效范围,从而重写一些相邻的内存。这是应该避免的,因为它会导致意想不到的结果或程序崩溃。
int myArray[2] = { 1, 2 };
myArray[2] = 3; // out of bounds error
要确定一个常规(静态分配)数组的长度,可以使用sizeof
操作符。
int length = sizeof(myArray) / sizeof(int); // 2
此方法不能用于动态分配的数组。确定这种数组大小的唯一方法是通过在数组分配中使用的变量。
int size = 3;
int* p = new int[size]; // dynamically allocated array
当你使用完一个动态数组时,你必须记得删除它。这是通过使用带有一组方括号的关键字delete
来完成的。
delete[] p; // release allocated array
八、字符串
C++ 中的string
类用于存储字符串值。在声明字符串之前,必须首先包含字符串头。也可以包含标准名称空间,因为 string 类是该名称空间的一部分。
#include <string>
using namespace std;
然后可以像声明任何其他数据类型一样声明字符串。要将字符串值赋给字符串变量,请用双引号将文本分隔开,然后将它们赋给变量。初始值也可以在声明字符串的同时通过构造器初始化来赋值。
string h = "Hello";
string w (" World");
字符串组合
加号,在这个上下文中称为连接操作符(+
),用于组合两个字符串。它有一个伴随的赋值操作符(+=
)来附加一个字符串。
string a = h + w; // Hello World
h += w; // Hello World
只要串联运算符所操作的字符串之一是 C++ 字符串,它就会起作用。
string b = "Hello" + w; // ok
它不能连接两个 C 字符串或两个字符串文字。为此,必须将其中一个值显式转换为一个string
。
char *c = "World"; // C-style string
b = (string)c + c; // ok
b = "Hello" + (string)" World"; // ok
如果省略了加号,字符串也将被隐式组合。
b = "Hel" "lo"; // ok
转义字符
通过在每一行的末尾加上反斜杠(\
)可以将字符串文字扩展到多行。
string s = "Hello \ World";
要向字符串本身添加新行,需要使用转义符“\n”。
s = "Hello \n World";
这个反斜杠符号用于书写特殊字符,如制表符或换页符。
此外,128 个 ASCII 字符中的任何一个都可以通过写一个反斜杠后跟该字符的 ASCII 码来表示,表示为八进制或十六进制数。
"\07F" // octal character (0-07F)
"\0x177" // hexadecimal character (0-0x177)
从 C++11 开始,通过在字符串前加上一个“R ”,并在双引号内加上一组括号,可以忽略转义字符。这称为原始字符串,例如,可以用来使文件路径更具可读性。
string escaped = "c:\\Windows\\System32\\cmd.exe";
string raw = R"(c:\Windows\System32\cmd.exe)";
字符串比较
比较两个字符串的方法很简单,就是使用等于运算符(==
)。这不会像 C 字符串那样比较字符串的内存地址。
string s = "Hello";
bool b = (s == "Hello"); // true
字符串函数
string
类有很多函数。其中最有用的是length
和size
函数,它们都返回字符串中的字符数。它们的返回类型是size_t
,这是一种无符号数据类型,用于保存对象的大小。这个只是一种内置数据类型的别名,但是它被定义为哪一种在不同的编译器之间是不同的。别名在 crtdefs.h 标准库文件中定义,该文件包含在 iostream 中。
size_t i = s.length(); // 5, length of string
i = s.size(); // 5, same as length()
另一个有用的函数是substr
(子串),它需要两个参数。第二个参数是从第一个参数中指定的位置开始返回的字符数。
s.substr(0,2); // "He"
也可以使用数组符号提取或更改单个字符。
char c = s[0]; // 'H'
字符串编码
双引号中的字符串产生一个 char 类型的数组,它只能容纳 256 个唯一的符号。为了支持更大的字符集,提供了宽字符类型 wchar_t。这种类型的字符串文字是通过在字符串前面加上大写字母“L”来创建的。可以使用 wstring 类存储生成的数组。这个类的工作方式类似于基本的 string 类,但是它使用 wchar_t 字符类型。
wstring s1 = L"Hello";
wchar_t *s2 = L"Hello";
C++11 中引入了固定大小的字符类型,即 char16_t 和 char32_t。这些类型分别提供了 UTF-16 和 UTF-32 编码的明确表示。UTF-16 字符串以“u”为前缀,可以使用 u16string 类存储。同样,UTF-32 字符串以“U”为前缀,存储在 u32string 类中。还添加了前缀“u8”来表示 UTF-8 编码的字符串文字。
string s3 = u8"UTF-8 string";
u16string s4 = u"UTF-16 string";
u32string s5 = U"UTF-32 string";
可以使用转义符“\u”后跟一个表示字符的十六进制数,将特定的 Unicode 字符插入字符串文字。
string s6 = u8"An asterisk: \u002A";
九、条件语句
条件语句用于根据不同的条件执行不同的代码块。
如果语句
if 语句只有在括号内的表达式计算结果为 true 时才会执行。在 C++ 中,这不一定是布尔表达式。它可以是计算结果为数字的任何表达式,在这种情况下,零为假,所有其他数字为真。
if (x < 1) {
cout << x << " < 1";
}
为了测试其他条件,if 语句可以由任意数量的 else if 子句扩展。
else if (x > 1) {
cout << x << " > 1";
}
if 语句的末尾可以有一个 else 子句,如果前面的所有条件都为假,将执行该子句。
else {
cout << x << " == 1";
}
至于花括号,如果只需要有条件地执行一条语句,就可以省去。但是,始终包含它们被认为是一种好的做法,因为它们可以提高可读性。
if (x < 1)
cout << x << " < 1";
else if (x > 1)
cout << x << " > 1";
else
cout << x << " == 1";
交换语句
switch 语句检查整数和一系列事例标签之间的相等性,然后将执行传递给匹配的事例。它可以包含任意数量的 case 子句,并且可以以处理所有其他 case 的默认标签结束。
switch (x)
{
case 0: cout << x << " is 0"; break;
case 1: cout << x << " is 1"; break;
default: cout << x << " is not 1 or 2"; break;
}
注意,每个 case 标签后的语句以关键字break
结束,以跳过开关的其余部分。如果省略了break
,执行将继续到下一个案例,如果需要以相同的方式评估几个案例,这将非常有用。
三元运算符
除了 if 和 switch 语句,还有三元运算符(?:
)可以替换单个 if/else 子句。这个运算符有三个表达式。如果第一个表达式为真,则计算并返回第二个表达式,如果为假,则计算并返回第三个表达式。
x = (x < 0.5) ? 0 : 1; // ternary operator (?:)
C++ 允许表达式作为独立的代码语句使用。因此,三元运算符不仅可以用作表达式,还可以用作语句。
(x < 0.5) ? x = 0 : x = 1; // alternative syntax
编程术语表达式指的是计算出一个值的代码,而语句是以分号或右花括号结束的代码段。
十、循环
C++ 中有三种循环结构,都用于多次执行一个特定的代码块。正如条件 if 语句一样,如果代码块中只有一条语句,则可以省略循环的花括号。
While 循环
只有当条件为真时,while 循环才会遍历代码块,并且只要条件保持为真,就会继续循环。请记住,条件只在每次迭代(循环)开始时检查。
int i = 0;
while (i < 10) { cout << i++; } // 0-9
Do-while 循环
do-while 循环的工作方式与 while 循环相同,只是它在代码块之后检查条件。因此,它将始终至少在代码块中运行一次。注意,这个循环以分号结束。
int j = 0;
do { cout << j++; } while (j < 10); // 0-9
用于循环
for 循环用于在代码块中运行特定的次数。它使用三个参数。第一个初始化一个计数器,并且总是在循环之前执行一次。第二个参数保存循环的条件,并在每次迭代之前进行检查。第三个参数包含计数器的增量,在每次循环结束时执行。
for (int k = 0; k < 10; k++) { cout << k; } // 0-9
for 循环有几种变体。首先,可以使用逗号操作符将第一个和第三个参数分成几个语句。
for (int k = 0, m = 0; k < 10; k++, m--) {
cout << k+m; // 0x10
}
也可以选择省略任何一个参数。
for (;;) {
cout << "infinite loop";
}
C++11 引入了基于范围的 for 循环语法,用于遍历数组和其他容器类型。在每次迭代中,数组中的下一个元素被绑定到引用变量,循环继续,直到遍历完整个数组。
int a[3] = {1, 2, 3};
for (int &i : a) {
cout <<i; // "123"
}
中断并继续
有两个跳转语句可以在循环内部使用:break
和continue
。break
关键字结束循环结构,continue
跳过当前迭代的剩余部分,并在下一次迭代的开始处继续。
for (int i = 0; i < 10; i++)
{
break; // end loop
continue; // start next iteration
}
Goto 语句
第三个跳转语句是goto
,它执行到指定标签的无条件跳转。这条指令通常不被使用,因为它会使执行流程难以遵循。
goto myLabel; // jump to label
myLabel: // label declaration
十一、函数
函数是可重用的代码块,只有在被调用时才会执行。
定义函数
可以通过键入void
后跟函数名、一组括号和一个代码块来创建函数。void
关键字意味着函数不会返回值。函数的命名惯例与变量相同——一个描述性的名称,除了第一个单词,每个单词最初都是大写的。
void myFunction()
{
cout << "Hello World";
}
调用函数
上面的函数在被调用时会简单地打印出一条文本消息。为了从主函数中调用它,函数的名字被指定,后跟一组括号。
int main()
{
myFunction(); // "Hello World"
}
函数参数
函数名后面的括号用于向函数传递参数。为此,必须首先将相应的参数以逗号分隔列表的形式添加到函数声明中。
void myFunction(string a, string b)
{
cout << a + " " + b;
}
一个函数可以被定义为接受任意数量的参数,并且它们可以有任意的数据类型。只要确保使用相同类型和数量的参数调用函数。
myFunction("Hello", "World"); // "Hello World"
准确的说, 参数出现在函数定义中,实参 出现在函数调用中。然而,这两个术语有时可以互换使用。
默认参数值
可以通过在参数列表中为参数赋值来指定默认值。
void myFunction(string a, string b = "Earth")
{
cout << a + " " + b;
}
然后,如果在调用函数时没有指定参数,将使用默认值。为此,有默认值的参数位于没有默认值的参数的右侧是很重要的。
myFunction("Hello"); // "Hello Earth"
函数重载
C++ 中的一个函数可以用不同的参数定义多次。这是一个被称为函数重载的强大特性,它允许一个函数处理各种参数,而使用该函数的程序员不需要知道它。
void myFunction(string a, string b) { cout << a+" "+b; }
void myFunction(string a) { cout << a; }
void myFunction(int a) { cout << a; }
返回语句
函数可以返回值。然后,void
关键字被替换为函数将返回的数据类型,并且return
关键字被添加到函数体中,后跟指定返回类型的参数。
int getSum(int a, int b)
{
return a + b;
}
Return 是一个跳转语句,它使函数退出,并将指定的值返回到调用函数的地方。例如,上面的函数可以作为参数传递给输出流,因为该函数的计算结果是一个整数。
cout << getSum(5, 10); // 15
return 语句也可以在 void 函数中使用,以便在到达结束块之前退出。
void dummy() { return; }
请注意,尽管 main 函数被设置为返回整数类型,但它不必显式返回值。这是因为编译器会自动在主函数的末尾添加一个 return zero 语句。
int main() { return 0; }
远期申报
在 C++ 中要记住的一件重要事情是,函数必须在被调用之前声明。这并不意味着函数必须在被调用之前实现。这只意味着需要在源文件的开头指定函数的头,这样编译器就知道该函数的存在。这种向前声明被称为原型。
void myFunction(int a); // prototype int main()
{
myFunction(0);
}
void myFunction(int a) {}
原型中的参数名称不需要包含在内。必须只指定数据类型。
void myFunction(int);
按值传递
在 C++ 中,默认情况下,原始数据类型和对象数据类型的变量都是通过值传递的。这意味着只有值或对象的副本被传递给函数。所以,以任何方式改变参数都不会影响原来的,传递大对象会很慢。
#include <iostream>
#include <string>
using namespace std;
void change(int i) { i = 10; }
void change(string s) { s = "Hello World"; }
int main()
{
int x = 0; // value type change(x); // value is passed
cout << x; // 0
string y = ""; // reference type
change(y); // object copy is passed
cout << y; // ""
}
通过引用传递
或者,改为通过引用传递变量,您只需要在函数定义中的参数名称前添加一个&符号。当参数通过引用传递时,原始数据类型和对象数据类型都可以被更改或替换,并且这些更改将影响原始数据类型。
void change(int& i) { i = 10; }
int main()
{
int x = 0; // value type
change(x); // reference is passed
cout << x; // 10
}
通过地址
作为通过引用传递的替代方法,也可以使用指针通过地址传递参数。这种传递技术与通过引用传递的目的相同,但使用的是指针语法。
void change(int* i) { *i = 10; }
int main()
{
int x = 0; // value type
change(&x); // address is passed
cout << x; // 10
}
通过值、引用或地址返回
除了通过值、引用或地址传递变量之外,变量也可以通过以下方式之一返回。最常见的是,函数通过值返回,在这种情况下,值的副本被返回给调用者。
int byVal(int i) { return i + 1; }
int main()
{
int a = 10;
cout << byVal(a); // 11
}
相反,为了通过引用返回,在函数的返回类型后放置一个&符号。然后,函数必须返回一个变量,不能像使用“按值返回”时那样返回表达式或文字。返回的变量不应该是局部变量,因为当函数结束时,这些变量的内存被释放。相反,通过引用返回通常用于返回也通过引用传递给函数的参数。
int& byRef(int& i) { return i; }
int main()
{
int a = 10;
cout << byRef(a); // 10
}
为了通过地址返回,解引用操作符被附加到函数的返回类型中。这种返回技术具有与通过引用返回时相同的两个限制——必须返回变量的地址,并且返回的变量不能是函数的局部变量。
int* byAdr(int* i) { return i; }
int main()
{
int a = 10;
cout << *byAdr(&a); // 10
}
内嵌函数
使用函数时要记住的一点是,每次调用函数时,都会产生性能开销。为了潜在地消除这种开销,您可以建议编译器通过使用inline
函数修饰符来内联对特定函数的调用。该关键字最适合在循环内部调用的小函数。它不应该用于较大的函数,因为内联这些函数会严重增加代码的大小,从而降低性能。
inline int myInc(int i) { return i++; }
注意,inline
关键字只是一个建议。编译器在试图优化代码时可能会选择忽略这个建议,也可能内联没有inline
修饰符的函数。
自动和 Decltype
C++11 中引入了两个新的关键字:auto 和 decltype 。这两个关键字都用于编译期间的类型推断。auto 关键字作为一个类型的占位符,指示编译器根据变量的初始化器自动推导出变量的类型。
auto i = 5; // int
auto d = 3.14; // double
auto b = false; // bool
Auto 转换为初始化器的核心类型,这意味着任何引用和常量说明符都被丢弃。
int& iRef = i;
auto myAuto = iRef; // int
可以根据需要手动重新应用删除的说明符。这里的&符号创建一个常规(左值)引用。
auto& myRef = iRef; // int&
或者,可以使用两个&符号。这通常指定一个右值引用,但是在 auto 的情况下,它让编译器根据给定的初始化器自动推导出一个右值或左值引用。
int i = 1;
auto&& a = i; // int& (lvalue reference)
auto&& b = 2; // int&& (rvalue reference)
自动说明符可以用在任何声明和初始化变量的地方。例如,下面的 For 循环迭代器的类型被设置为 auto,因为编译器可以很容易地推断出类型。
#include <vector>
using namespace std;
// ...
vector<int> myVector { 1, 2, 3 };
for (auto& x : myVector) { cout << x; } // "123"
在 C++11 之前,没有基于范围的 for 循环或 auto 说明符。迭代一个向量需要更冗长的语法。
for(vector<int>::size_type i = 0; i != myVector.size(); i++) {
cout << myVector[i]; // "123"
}
decltype 说明符的工作方式类似于 auto,只是它推导出给定表达式的确切声明类型,包括引用。该表达式在括号中指定。
decltype(3) b = 3; // int&&
在 C++14 中,auto 可以用作 decltype 的表达式。然后用初始化表达式替换关键字 auto,这样就可以推导出初始化器的确切类型。
decltype(auto) = 3; // int&&
当初始化器可用时,使用 auto 通常是更简单的选择。Decltype 主要用于转发函数返回类型,不用考虑是引用类型还是值类型。
decltype(5) getFive() { return 5; } // int
C++11 增加了尾随返回类型语法,允许在参数列表后指定函数的返回值,跟在箭头操作符(->)之后。这使得在用 decltype 推导返回类型时可以使用该参数。在 C++11 的上下文中使用 auto 仅仅意味着使用了尾随返回类型语法。
auto getValue(int x) -> decltype(x) { return x; } // int
C++14 中增加了使用 auto 进行返回类型演绎的函数。这使得核心返回类型可以直接从 return 语句中推导出来,
auto getValue(int x) { return x; } // int
此外,auto 可以与 decltype 一起使用,按照 decltype 的规则推导出确切的类型。
decltype(auto) getRef(int& x) { return x; } // int&
类型推导的主要用途是减少代码的冗长性并提高可读性,尤其是在声明复杂类型时,在这种情况下,类型要么难以识别,要么难以编写。请记住,在现代 ide 中,您可以将鼠标悬停在一个变量上来检查它的类型,即使该类型是自动推导出来的。
λ函数
C++11 增加了创建 lambda 函数的能力,这些函数是未命名的函数对象。这提供了一种在使用点定义函数的简洁方法,而不必在其他地方创建命名函数。下面的示例创建一个 lambda,它接受两个 int 参数并返回它们的总和。
auto sum = [](int x, int y) -> int
{
return x + y;
};
cout << sum(2, 3); // "5"
如果编译器可以从 lambda 推导出返回值,那么包含返回类型是可选的。在 C++11 中,这要求 lambda 只包含一个返回语句,而 C++14 将返回类型演绎扩展到任何 lambda 函数。注意,省略返回类型时,箭头操作符(->)也被省略。
auto sum = [](int x, int y) { return x + y; };
C++11 要求用具体类型声明 lambda 参数。这个要求在 C++14 中被放宽了,允许 lambdas 使用自动类型演绎。
auto sum = [](auto x, auto y) { return x + y; };
Lambdas 通常用于指定只引用一次的简单函数,通常是通过将 function 对象作为参数传递给另一个函数。这可以通过使用具有匹配参数列表和返回类型的函数包装来完成,如下例所示。
#include <iostream>
#include <functional>
using namespace std;
void call(int arg, function<void(int)> func) {
func(arg);
}
int main() {
auto printSquare = [](int x) { cout << x*x; };
call(2, printSquare); // "4"
}
所有的 lambdas 都以一组方括号开始,称为 capture 子句。该子句指定了可以在 lambda 主体中使用的周围范围的变量。这有效地将额外的参数传递给 lambda,而不需要在函数包装的参数列表中指定这些参数。因此,前面的例子可以用下面的方式重写。
void call(function<void()> func) { func(); }
int main() {
int i = 2;
auto printSquare = [i]() { cout << i*i; };
call(printSquare); // "4"
}
这里的变量是通过值捕获的,所以在 lambda 中使用了一个副本。变量也可以通过引用使用熟悉的&前缀来捕获。注意,lambda 在这里是在同一个语句中定义和调用的。
int a = 1;
&a { a += x; }(2);
cout << a; // "3"
可以指定一个默认的捕获模式,以指示如何捕获 lambda 中使用的任何未指定的变量。[=]表示变量是通过值捕获的,而[&]是通过引用捕获的。由值捕获的变量通常是常量,但是可变说明符可以用来允许修改这样的变量。
int a = 1, b = 1;
[&, b]() mutable { b++; a += b; }();
cout << a << b; // "31"
从 C++14 开始,变量也可以在 capture 子句中初始化。如果在外部作用域中没有同名的变量,那么变量的类型将被自动推导出来。
int a = 1;
[&, b = 2]() { a += b; }();
cout << a; // "3"
十二、类
类是用于创建对象的模板。要定义一个,使用关键字class
,后跟一个名称、一个代码块和一个分号。类的命名约定是混合大小写,这意味着每个单词最初都应该大写。
class MyRectangle {};
类成员可以在类内部声明;两种主要类型是字段和方法。字段是变量,它们保存对象的状态。方法是函数,它们定义了对象能做什么。
class MyRectangle
{
int x, y;
};
类方法
属于一个类的方法通常被声明为该类内部的原型,而实际的实现放在该类的定义之后。然后,类之外的方法名需要以类名和范围解析操作符为前缀,以指定方法定义属于哪个类。
class MyRectangle
{
int x, y;
int getArea();
};
int MyRectangle::getArea() { return x * y; }
内嵌方法
如果方法很短,并且您想建议编译器将函数的代码插入(内联)到调用者的代码中,一种方法是在方法的定义中使用inline
关键字。
inline int MyRectangle::getArea() { return x * y; }
更方便的方法是简单地在类内部定义方法。这将隐式地向编译器建议应该内联该方法。
class MyRectangle
{
int x, y;
int getArea() { return x * y; }
};
对象创建
类定义现在完成了。为了使用它,你首先必须创建一个类的对象,也称为实例。这可以通过声明变量的相同方式来实现。
int main()
{
MyRectangle r; // object creation
}
访问对象成员
在访问该对象包含的成员之前,首先需要在类定义中将它们声明为 public,方法是使用关键字public
后跟一个冒号。
class MyRectangle
{
public:
int x, y;
int getArea() { return x * y; }
};
现在可以在实例名称后使用点运算符(.
)来访问该对象的成员。
r.x = 10;
r.y = 5;
int z = r.getArea(); // 50 (5*10)
基于一个类可以创建任意数量的对象,每个对象都有自己的一组字段和方法。
MyRectangle r2; // another instance of MyRectangle
r2.x = 25; // not same as r.x
使用对象指针时,箭头操作符(->
)允许访问对象的成员。该运算符的行为类似于点运算符,只是它首先取消对指针的引用。它专门用于指向对象的指针。
MyRectangle r;
MyRectangle *p = &r; // object pointer
p->getArea();
(*p).getArea(); // alternative syntax
远期申报
类和函数一样,必须在被引用之前声明。如果一个类定义没有出现在对该类的第一次引用之前,那么可以在引用之上指定一个类原型。
class MyClass; // class prototype
这种向前声明允许在任何不需要完全定义该类的上下文中引用该类。
class MyClass; // class prototype
MyClass* p; // allowed
MyClass f(MyClass&); // allowed
MyClass o; // error, definition required
sizeof(MyClass); // error, definition required
注意,即使有了原型,你仍然不能在定义之前创建一个类的对象。
十三、构造器
除了字段和方法,一个类可以包含一个构造器。这是一种特殊的方法,用于构造或者实例化对象。它总是与类同名,并且没有返回类型。要从另一个类访问,需要在标记有public
访问修饰符的部分声明构造器。
class MyRectangle
{
public:
int x, y; MyRectangle();
};
MyRectangle::MyRectangle() { x = 10; y = 5; }
当创建该类的新实例时,将调用构造器方法,在这种情况下,该方法为字段分配默认值。
int main()
{
MyRectangle s;
}
构造器重载
与任何其他方法一样,构造器可以重载。这将允许用不同的参数列表创建一个对象。
class MyRectangle
{
public:
int x, y; MyRectangle(); MyRectangle(int, int);
};
MyRectangle::MyRectangle() { x = 10; y = 5; }
MyRectangle::MyRectangle(int a, int b) { x = a; y = b; }
例如,使用上面定义的两个构造器,对象可以不带参数初始化,也可以带两个参数初始化,这两个参数将用于分配字段。
// Calls parameterless constructor
MyRectangle r;
// Calls constructor accepting two integers
MyRectangle t(2,3);
C++11 增加了构造器调用其他构造器的能力。使用这个特性,前面创建的无参数构造器在这里被重新定义来调用第二个构造器。
MyRectangle::MyRectangle(): MyRectangle(10, 5);
这个关键字
在构造器内部,以及在属于对象的其他方法中——所谓的实例方法——可以使用一个叫做this
的特殊关键字。这是指向该类的当前实例的指针。例如,如果构造器的参数名与字段名相同,这将非常有用。这些字段仍然可以通过使用this
指针来访问,即使它们被参数所掩盖。
MyRectangle::MyRectangle(int x, int y)
{
this->x = x; this->y = y;
}
字段初始化
作为在构造器内部分配字段的替代方法,它们也可以通过使用构造器初始化列表来分配。该列表以构造器参数后的冒号开头,后面是对字段自身构造器的调用。这实际上是通过构造器分配字段的推荐方式,因为它比在构造器内分配字段提供了更好的性能。
MyRectangle::MyRectangle(int a, int b) : x(a), y(b) {}
字段也可以在它们的类定义中被赋予一个初始值,这是 C++11 中添加的一个方便的特性。当创建新实例时,在运行构造器之前,会自动分配该值。因此,这种赋值可用于为可能在构造器中被重写的字段指定默认值。
class MyRectangle
{
public:
// Class member initialization
int x = 10;
int y = 5;
};
默认构造器
如果没有为一个类定义构造器,编译器会在程序编译时自动创建一个缺省的无参数构造器。因此,即使没有实现构造器,类也可以被实例化。默认构造器将只为对象分配内存。它不会初始化字段。与全局变量不同,C++ 中的字段不会自动初始化为默认值。这些字段将包含留在它们的内存位置中的任何垃圾,直到它们被显式赋值。
破坏者
除了构造器,一个类还可以有一个显式定义的析构函数。析构函数用于释放对象分配的任何资源。在对象被销毁之前,或者当对象超出范围时,或者当它被用new
操作符创建的对象显式删除时,它被自动调用。析构函数的名称与类名相同,但前面有一个波浪号(~
)。一个类只能有一个析构函数,它从不接受任何参数或返回任何东西。
class Semaphore
{
public:
bool *sem;
Semaphore() { sem = new bool; }
~Semaphore() { delete sem; }
};
特殊成员功能
默认的构造器和析构函数都是特殊的成员函数,编译器会自动提供给没有明确定义它们的类。另外两个这样的方法是复制构造器和复制赋值操作符(operator =)。C++11 标准提供了通过删除和默认说明符来控制是否允许这些特殊成员函数的方法。delete 说明符禁止调用函数,而 default 说明符明确声明将使用编译器生成的默认值。
class A
{
public:
// Explicitly include default constructor
A() = default;
A(int i);
// Disable copy constructor
A(const A&) = delete;
// Disable copy assignment operator
A& operator=(const A&) = delete;
};
对象初始化
C++ 提供了许多不同的方法来创建对象和初始化它们的字段。下面的类将用来说明这些方法。
class MyClass
{
public:
int i;
MyClass() = default;
MyClass(int x) : i(x) {}
};
直接初始化
到目前为止一直使用的对象创建语法叫做直接初始化。该语法可以包括一组括号,用于将参数传递给类中的构造器。如果使用无参数构造器,括号将被省略。
// Direct initialization
MyClass a(5); MyClass b;
值初始化
一个对象也可以被值初始化。然后,通过使用类名后跟一组括号来创建对象。圆括号可以提供构造器参数,或者保留为空以使用无参数构造器构造对象。值初始化只创建一个临时对象,该对象在语句结束时被销毁。要保留该对象,必须将其复制到另一个对象或指定给一个引用。将临时对象分配给引用将维护该对象,直到该引用超出范围。
// Value initialization
const MyClass& a = MyClass();
MyClass&& b = MyClass();
初始化对象值与使用默认初始化创建对象值几乎相同。一个微小的区别是,在某些情况下,当使用值初始化时,非静态字段将被初始化为它们的默认值。
复制初始化
如果一个现有对象在声明时被分配给一个相同类型的对象,新对象将被复制初始化。这意味着现有对象的每个成员都将被复制到新对象中。
// Copy initialization
MyClass a = MyClass();
MyClass b(a);
MyClass c = b;
这是因为编译器提供了隐式的复制构造器,这种类型的赋值都会调用它。复制构造器接受其自身类型的单个参数,然后构造指定对象的副本。请注意,这种行为不同于许多其他语言,如 Java 和 C#。在这些语言中,用另一个对象初始化一个对象只会复制该对象的引用,而不会创建新的对象副本。
新初始化
一个对象可以通过使用new
关键字的动态内存分配来初始化。动态分配的内存必须通过指针或引用来使用。new
操作符返回一个指针,所以要将它赋给一个引用,首先需要取消引用。请记住,一旦不再需要动态分配的内存,就必须显式释放它。
// New initialization
MyClass* a = new MyClass(); MyClass& b = *new MyClass();
// ...
delete a, b;
聚合初始化
初始化名为聚合初始化的对象时,有一个语法快捷方式可用。这种语法允许使用一个用大括号括起来的初始值设定项列表来设置字段,就像使用数组一样。只有当类类型不包含任何构造器、虚函数或基类时,才能使用聚合初始化。这些字段也必须是公共的,除非它们被声明为静态的。每个字段将按照它们在类中出现的顺序进行设置。
// Aggregate initialization
MyClassa = { 2 }; // iis 2
统一初始化
统一初始化是在 C++11 中引入的,目的是提供一种一致的方法来初始化类型,这种方法对任何类型都适用。该语法看起来与聚合初始化相同,只是没有使用等号。
// Uniform initialization
MyClass a { 3 }; // i is 3
这种初始化语法不仅适用于类,还适用于任何类型,包括原语、字符串、数组和 vector 等标准库容器。
#include <string>
#include <vector>
using namespace std;
int i { 1 };
string s {"Hello"};
int a[] { 1, 2 };
int *p= new int [2] { 1, 2 };
vector<string> box { "one", "two" };
统一初始化可用于调用构造器。这是通过为该构造器传递适当的参数自动完成的。
// Call parameterless constructor
MyClass b {};
// Call copy constructor
MyClass c { b };
一个类可以定义一个初始化列表构造器。如果为 initializer_list 模板指定的类型与用大括号括起来的参数列表的类型相匹配,则该构造器在统一初始化期间被调用,并且优先于其他形式的构造。论点单可以是任意长度,但所有元素必须是同一类型。在下面的例子中,列表的类型是 int,因此用于构造该对象的整数列表被传递给构造器。然后使用基于范围的 for 循环显示这些整数。
#include <iostream>
using namespace std;
class NewClass
{
public:
NewClass(initializer_list<int> args)
{
for (auto x : args)
cout << x << " ";
}
};
int main()
{
NewClass a { 1, 2, 3 }; // "1 2 3"
}
十四、继承
继承允许一个类获得另一个类的成员。在下面的示例中,Square 继承了 Rectangle。这是在类名后指定的,使用冒号后跟public
关键字和要继承的类名。然后 Rectangle 成为 Square 的基类,Square 又成为 Rectangle 的派生类。除了它自己的成员,Square 还获得了 Rectangle 中所有可访问的成员,除了它的构造器和析构函数。
class Rectangle
{
public:
int x, y;
int getArea() { return x * y; }
};
class Square : public Rectangle {};
向上抛
一个对象可以被向上转换为它的基类,因为它包含了基类所包含的一切。向上转换是通过将对象分配给其基类类型的引用或指针来执行的。在下面的例子中,一个正方形对象被向上投射为矩形。当使用 Rectangle 的接口时,Square 对象将被视为一个矩形,因此只能访问 Rectangle 的成员。
Square s;
Rectangle& r = s; // reference upcast
Rectangle* p = &s; // pointer upcast
派生类可以用在任何需要基类的地方。例如,正方形对象可以作为参数传递给需要矩形对象的函数。然后,派生的对象将隐式地向上转换为它的基类型。
void setXY(Rectangle& r) { r.x = 2; r.y = 3; }
int main()
{
Square s;
setXY(s);
}
向下铸造
指向正方形对象的矩形引用可以向下转换回正方形对象。这种向下转换必须是显式的,因为不允许将实际的矩形向下转换为正方形。
Square& a = (Square&) r; // reference downcast
Square& b = (Square&) *p; // pointer downcast
构造器继承
为了确保基类中的字段被正确初始化,当派生类的对象被创建时,基类的无参数构造器被自动调用。
class B1
{
public:
int x;
B1() : x(5) {}
};
class D1 : public B1 {};
int main()
{
// Calls parameterless constructors of D1 and B1
D1 d;
cout << d.x; // "5"
}
通过将派生构造器放在构造器的初始化列表中,可以从派生构造器显式调用基构造器。这允许将参数传递给基构造器。
class B2
{
public:
int x;
B2(int a) : x(a) {}
};
class D2 : public B2
{
public:
D2(int i) : B2(i) {} // call base constructor
};
在这种情况下,另一种解决方案是继承构造器。从 C++11 开始,这可以通过 using 语句来完成。
class D2 : public B2
{
public:
using B2::B2; // inherit all constructors
int y{0};
};
请注意,基类构造器不能初始化派生类中定义的字段。因此,派生类中声明的任何字段都应该自己初始化。这是使用统一符号来完成的。
多重继承
C++ 允许派生类从多个基类继承。这叫做多重继承。基类在逗号分隔的列表中指定。
class Person {}
class Employee {}
class Teacher: public Person, public Employee {}
多重继承并不常用,因为大多数现实世界的关系可以用单一继承来更好地描述。它还会显著增加代码的复杂性。
十五、覆盖
派生类中的新方法可以重新定义基类中的方法,以便为其提供新的实现。
隐藏派生成员
在下面的例子中,Rectangle 的getArea
方法用相同的签名在 Triangle 中重新声明。签名包括方法的名称、参数列表和返回类型。
class Rectangle
{
public:
int x, y;
int getArea() { return x * y; }
};
class Triangle : public Rectangle
{
public:
Triangle(int a, int b) { x = a; y = b; }
int getArea() { return x * y / 2; }
};
如果创建了一个三角形对象并调用了getArea
方法,那么三角形的方法版本将被调用。
Triangle t = Triangle(2,3);
t.getArea(); // 3 (2*3/2) calls Triangle’s version
然而,如果三角形被向上转换为矩形,那么矩形的版本将被调用。
Rectangle& r = t;
r.getArea(); // 6 (2*3) calls Rectangle’s version
这是因为重定义的方法只隐藏了继承的方法。这意味着 Triangle 的实现在类层次结构中被向下重定义到 Triangle 的任何子类,而不是向上重定义到基类。
覆盖派生成员
为了在类层次结构中向上重新定义一个方法,也就是所谓的覆盖,这个方法需要在基类中用virtual
修饰符来声明。此修饰符允许在派生类中重写该方法。
class Rectangle
{
public:
int x, y;
virtual int getArea() { return x * y; }
};
从 Rectangle 的接口调用getArea
方法将会调用 Triangle 的实现。
Rectangle& r = t;
r.getArea(); // 3 (2*3/2) calls Triangle’s version
C++11 添加了覆盖说明符,它表明一个方法旨在替换一个继承的方法。使用该说明符允许编译器检查是否存在具有相同签名的虚方法。这防止了意外创建新的虚方法的可能性。
virtual float getArea() override {} // error - no base class method to override
C++11 中引入的另一个说明符是 final。此说明符防止在派生类中重写虚方法。它还防止派生类使用相同的方法签名。
class Base
{
virtual void foo() final {}
}
class Derived
{
void foo() {} // error: Base::foo marked as final
}
final 说明符也可以应用于一个类,以防止任何类继承它。
class B final {}
class D : B {} // error: B marked as final
基类范围
通过键入类名后跟范围解析运算符,仍然可以从派生类中访问重新定义的方法。这被称为基类作用域 ,可用于允许访问类层次结构中任意深度的重定义方法。
class Triangle : public Rectangle
{
public:
Triangle(int a, int b) { x = a; y = b; }
int getArea() { return Rectangle::getArea() / 2; }
};