面向零基础初学者的现代 C++ 教程(二)
十七、常量
当我们想要一个只读对象或者保证不改变当前作用域中某个对象的值时,我们就把它设为常量。C++ 使用const类型限定符将对象标记为只读。我们说我们的对象现在是不可变的 ??。例如,要定义一个值为 5 的整数常量,我们可以这样写:
int main()
{
const int n = 5;
}
我们现在可以在诸如数组大小的地方使用该常量:
int main()
{
const int n = 5;
int arr[n] = { 10, 20, 30, 40, 50 };
}
常量是不可修改的,尝试这样做会导致编译时错误:
int main()
{
const int n = 5;
n++; // error, can’t modify a read-only object
}
不能给声明为const的对象赋值;它需要初始化。所以,我们不能有:
int main()
{
const int n; // error, no initializer
const int m = 123; // OK
}
值得注意的是const修改了整个类型,而不仅仅是对象。所以,const int和int是两种不同的类型。第一个据说是const——合格。
另一个 const 限定符是名为constexpr的常量表达式。它是一个可以在编译时计算的常量。常量表达式的初始值设定项可以在编译时计算,并且本身必须是常量表达式。示例:
int main()
{
constexpr int n = 123; //OK, 123 is a compile-time constant // expression
constexpr double d = 456.78; //OK, 456.78 is a compile-time constant // expression
constexpr double d2 = d; //OK, d is a constant expression
int x = 123;
constexpr int n2 = x; //compile-time error
// the value of x is not known during // compile-time
}
十八、练习
18.1 简单的 if 语句
写一个程序,定义一个值为假的布尔变量。使用变量作为 if 语句中的条件。
#include <iostream>
int main()
{
bool mycondition = false;
if (mycondition)
{
std::cout << "The condition is true." << '\n';
}
else
{
std::cout << "The condition is not true." << '\n';
}
}
18.2 逻辑运算符
写一个程序,定义一个 int 类型的变量。将值 256 赋给变量。检查此变量的值是否大于 100 且小于 300。然后,定义一个值为 true 的布尔变量。检查 int 数是否大于 100,或者 bool 变量的值是否为 true。然后定义第二个 bool 变量,其值将是第一个 bool 变量的反值。
#include <iostream>
int main()
{
int x = 256;
if (x > 100 && x < 300)
{
std::cout << "The value is greater than 100 and less than 300." << '\n';
}
else
{
std::cout << "The value is not inside the (100 .. 300) range." << '\n';
}
bool mycondition = true;
if (x > 100 || mycondition)
{
std::cout << "Either x is greater than 100 or the bool variable is true." << '\n';
}
else
{
std::cout << "x is not greater than 100 and the bool variable is false." << '\n';
}
bool mysecondcondition = !mycondition;
}
18.3 转换声明
写一个程序,定义一个值为 3 的简单整数变量。使用 switch 语句检查该值是否在[1..4]范围。
#include <iostream>
int main()
{
int x = 3;
switch (x)
{
case 1:
std::cout << "The value is equal to 1." << '\n';
break;
case 2:
std::cout << "The value is equal to 2." << '\n';
break;
case 3:
std::cout << "The value is equal to 3." << '\n';
break;
case 4:
std::cout << "The value is equal to 4." << '\n';
break;
default:
std::cout << "The value is not inside the [1..4] range." << '\n';
break;
}
}
18.4 for 循环
写一个程序,使用 for 循环打印计数器 15 次。计数器从 0 开始计数。
#include <iostream>
int main()
{
for (int i = 0; i < 15; i++)
{
std::cout << "The counter is now: " << i << '\n';
}
}
18.5 数组和 for 循环
写一个定义 5 个整数的数组的程序。使用 for 循环打印数组元素及其索引。
#include <iostream>
int main()
{
int arr[5] = { 3, 20, 8, 15, 10 };
for (int i = 0; i < 5; i++)
{
std::cout << "arr[" << i << "] = " << arr[i] << '\n';
}
}
说明:这里,我们定义了一个包含 5 个元素的数组。数组从零开始索引。因此第一个数组元素 3 的索引为 0。最后一个数组元素 10 的索引为 4。我们使用 for 循环迭代数组元素,并打印它们的索引和值。我们的 for 循环从计数器 0 开始,以计数器 4 结束。
18.6 常量类型限定符
编写一个程序,分别定义三个类型为 const int、const double 和 const std::string 的对象。定义第四个 const int 对象,并用第一个 const int 对象的值初始化它。打印出所有变量的值。
#include <iostream>
int main()
{
const int c1 = 123;
const double d = 456.789;
const std::string s = "Hello World!";
const int c2 = c1;
std::cout << "Constant integer c1 value: " << c1 << '\n';
std::cout << "Constant double d value: " << d << '\n';
std::cout << "Constant std::string s value: " << s << '\n';
std::cout << "Constant integer c2 value: " << c2 << '\n';
}
十九、函数
19.1 简介
我们可以将 C++ 代码分成更小的块,称为函数。函数在声明中有一个返回类型、一个名称、一个参数列表,在定义中还有一个额外的函数体。一个简单的函数定义是:
type function_name(arguments) {
statement;
statement;
return something;
}
19.2 函数声明
要声明一个函数,我们需要指定一个返回类型、一个名称和一个参数列表,如果有的话。要声明一个不接受任何参数的类型为void的名为myfunction的函数,我们编写:
void myvoidfunction();
int main()
{
}
Type void是一个表示 nothing 的类型,是一组空的值。要声明一个接受一个参数的类型为int的函数,我们可以写:
int mysquarednumber (int x);
int main()
{
}
要声明一个类型为int的函数,例如,它接受两个int参数,我们可以写:
int mysum(int x, int y);
int main()
{
}
仅在函数声明中,我们可以省略参数名,但是我们需要指定它们的类型:
int mysum(int, int);
int main()
{
}
19.3 函数定义
要在程序中被调用,必须首先定义一个函数。函数定义拥有函数声明所拥有的一切,再加上函数体。它们是返回类型、函数名、函数参数列表(如果有的话)和函数体。示例:
#include <iostream>
void myfunction(); // function declaration
int main()
{
}
// function definition
void myfunction() {
std::cout << "Hello World from a function.";
}
要定义一个接受一个参数的函数,我们可以写:
int mysquarednumber(int x); // function declaration
int main()
{
}
// function definition
int mysquarednumber(int x) {
return x * x;
}
要定义一个接受两个参数的函数,我们可以写:
int mysquarednumber(int x); // function declaration
int main()
{
}
// function definition
int mysquarednumber(int x) {
return x * x;
}
为了在我们的程序中调用这个函数,我们指定函数名后跟空括号,因为这个函数没有参数:
#include <iostream>
void myfunction(); // function declaration
int main()
{
myfunction(); // a call to a function
}
// function definition
void myfunction() {
std::cout << "Hello World from a function.";
}
要调用接受一个参数的函数,我们可以使用:
#include <iostream>
int mysquarednumber(int x); // function declaration
int main()
{
int myresult = mysquarednumber(2); // a call to the function
std::cout << "Number 2 squared is: " << myresult;
}
// function definition
int mysquarednumber(int x) {
return x * x;
}
我们通过名字调用函数mysquarednumber,提供一个值2来代替函数参数,并将函数的结果赋给我们的myresult变量。我们传递给函数的内容通常被称为函数参数。
要调用一个接受两个或更多参数的函数,我们使用函数名,后跟一个左括号,再跟一列用逗号分隔的参数,最后是右括号。示例:
#include <iostream>
int mysum(int x, int y);
int main()
{
int myresult = mysum(5, 10);
std::cout << "The sum of 5 and 10 is: " << myresult;
}
int mysum(int x, int y) {
return x + y;
}
19.4 退货声明
函数属于某种类型,也称为返回类型、,它们必须返回值。返回值由一个return-语句指定。类型为void的函数不需要return语句。示例:
#include <iostream>
void voidfn();
int main()
{
voidfn();
}
void voidfn()
{
std::cout << "This is void function and needs no return.";
}
其他类型的函数(main 函数除外)需要一个return-语句:
#include <iostream>
int intfn();
int main()
{
std::cout << "The value of a function is: " << intfn();
}
int intfn()
{
return 42; // return statement
}
如果需要,一个函数可以有多个return-语句。一旦任何一个return-语句被执行,函数就停止,函数中的其余代码被忽略:
#include <iostream>
int multiplereturns(int x);
int main()
{
std::cout << "The value of a function is: " << multiplereturns(25);
}
int multiplereturns(int x)
{
if (x >= 42)
{
return x;
}
return 0;
}
19.5 传递参数
向函数传递参数有不同的方式。在这里,我们将描述三个最常用的。
19.5.1 按值/副本传递
当我们将一个参数传递给一个函数时,如果函数的参数类型不是一个引用,那么我们将复制该参数并传递给函数。这意味着原始参数的值不会改变。将制作一份该参数的副本。示例:
#include <iostream>
void myfunction(int byvalue)
{
std::cout << "Argument passed by value: " << byvalue;
}
int main()
{
myfunction(123);
}
这被称为通过值传递参数或者通过拷贝传递参数*。*
通过引用传递
当函数参数类型是引用类型时,实际的实参被传递给函数。该函数可以修改参数的值。示例:
#include <iostream>
void myfunction(int& byreference)
{
byreference++; // we can modify the value of the argument
std::cout << "Argument passed by reference: " << byreference;
}
int main()
{
int x = 123;
myfunction(x);
}
这里我们传递了一个引用类型的参数int&,所以这个函数现在使用实际的参数,并且可以改变它的值。当通过引用传递时,我们需要传递变量本身;我们不能传入表示值的文字。最好避免通过引用传递。
19.5.3 通过常量引用
优选的是通过常量引用 ,也称为对常量的引用来传递参数。通过引用传递参数可能更有效,但为了确保它不被更改,我们将它设为 const 引用类型。示例:
#include <iostream>
#include <string>
void myfunction(const std::string& byconstreference)
{
std::cout << "Arguments passed by const reference: " << byconstreference;
}
int main()
{
std::string s = "Hello World!";
myfunction(s);
}
出于效率的原因,我们使用通过常量引用传递,并且const修饰符确保参数的值不会被改变。
在最后三个例子中,我们省略了函数声明,只提供了函数定义。虽然函数定义也是一个声明,但是您应该同时提供声明和定义,如下所示:
#include <iostream>
#include <string>
void myfunction(const std::string& byconstreference);
int main()
{
std::string s = "Hello World!";
myfunction(s);
}
void myfunction(const std::string& byconstreference)
{
std::cout << "Arguments passed by const reference: " << byconstreference;
}
19.6 函数重载
我们可以有多个同名但参数类型不同的函数。这叫做函数重载。一个简单的解释:当函数名相同,但参数类型不同时,我们就有了重载函数。函数重载声明的示例:
void myprint(char param);
void myprint(int param);
void myprint(double param);
然后我们实现函数定义并调用每个函数定义:
#include <iostream>
void myprint(char param);
void myprint(int param);
void myprint(double param);
int main()
{
myprint('c'); // calling char overload
myprint(123); // calling integer overload
myprint(456.789); // calling double overload
}
void myprint(char param)
{
std::cout << "Printing a character: " << param << '\n';
}
void myprint(int param)
{
std::cout << "Printing an integer: " << param << '\n';
}
void myprint(double param)
{
std::cout << "Printing a double: " << param << '\n';
}
当调用我们的函数时,会根据我们提供的参数类型选择适当的重载。在对myprint('c'),的第一次调用中,选择了一个char重载,因为文字'c'是类型char.在第二次函数调用myprint(123),中,选择了一个整数重载,因为参数123是类型int.最后,在我们的最后一次函数调用myprint(456.789),中,编译器选择了一个双重载,因为参数456.789是类型double。
是的,C++ 中的文字也有类型,C++ 标准精确地定义了它是什么类型。一些文字及其对应的类型:
'c' - char
123 - int
456.789 - double
true - boolean
"Hello" - const char[6]
二十、练习
20.1 函数定义
编写一个程序,定义一个名为printmessage()的void类型的函数。该函数在标准输出上输出一条"Hello World from a function."消息。从main调用函数。
#include <iostream>
void printmessage()
{
std::cout << "Hello World from a function.";
}
int main()
{
printmessage();
}
20.2 单独声明和定义
编写一个程序,声明并定义一个名为printmessage()的void类型的函数。该函数在标准输出上输出一条"Hello World from a function."消息。从main调用函数。
#include <iostream>
void printmessage(); // function declaration
int main()
{
printmessage();
}
// function definition
void printmessage()
{
std::cout << "Hello World from a function.";
}
20.3 函数参数
写一个程序,它有一个类型为int的函数,名为multiplication,通过值接受两个int参数。该函数将这两个参数相乘,并将结果返回给自身。调用 main 中的函数,并将函数的结果赋给一个本地int变量。在控制台中打印结果。
#include <iostream>
int multiplication(int x, int y)
{
return x * y;
}
int main()
{
int myresult = multiplication(10, 20);
std::cout << "The result is: " << myresult;
}
20.4 传递参数
编写一个程序,该程序有一个名为custommessage的void类型的函数。该函数通过引用类型std::string的const来接受一个参数,并使用该参数的值在标准输出上输出一个定制消息。用本地字符串调用 main 中的函数。
#include <iostream>
#include <string>
void custommessage(const std::string& message)
{
std::cout << "The string argument you used is: " << message;
}
int main()
{
std::string mymessage = "My Custom Message.";
custommessage(mymessage);
}
20.5 函数重载
写一个有两个函数重载的程序。这些函数被称为division,,都接受两个参数。它们对参数进行除法运算,并将结果返回给自己。第一个函数重载的类型是int,并且有两个类型为int的参数。第二个重载类型为double,接受两个类型为double的参数。调用main中适当的重载,首先提供整数参数,然后是double参数。观察不同的结果。
#include <iostream>
#include <string>
int division(int x, int y)
{
return x / y;
}
double division(double x, double y)
{
return x / y;
}
int main()
{
std::cout << "Integer division: " << division(9, 2) << '\n';
std::cout << "Floating point division: " << division(9.0, 2.0);
}
二十一、范围和生存期
当我们声明一个变量时,它的名字只在源代码的某些部分有效。而源代码的那一段(部件、部分、区域)叫做范围。它是可以访问名称的代码区域。有不同的范围:
21.1 当地范围
当我们在函数中声明一个名字时,这个名字有一个局部作用域。它的作用域从声明点到标有}的函数块的末尾。
示例:
void myfunction()
{
int x = 123; // Here begins the x's scope
} // and here it ends
我们的变量x是在myfunction()体中声明的,它有一个局部范围。我们说 x 这个名字是 ?? 本地的。它只存在于(可以被访问)函数的作用域内,而不存在于其他地方。
21.2 区块范围
block-scope 是由以{开始,以}结束的代码块标记的一段代码。示例:
int main()
{
int x = 123; // first x' scope begins here
{
int x = 456; // redefinition of x, second x' scope begins here
} // block ends, second x' scope ends here
// the first x resumes here
} // block ends, scope of first x's ends here
还有其他的作用域,我们将在本书后面介绍。在这一点上引入作用域的概念来解释对象的生存期是很重要的。
21.3 生存期
对象的生存期是对象在内存中花费的时间。生存期由所谓的存储持续时间决定。有不同种类的存储持续时间。
21.4 自动存储持续时间
自动存储持续时间是在代码块开始时自动分配对象的内存,并在代码块结束时释放内存的持续时间。这也被称为一个栈存储器;对象被分配到栈中。在这种情况下,对象的生存期由其范围决定。所有本地对象都有这个存储持续时间。
21.5 动态存储持续时间
动态存储持续时间是对象的存储器被手动分配和手动解除分配的持续时间。这种存储通常被称为堆内存。用户决定何时为对象分配内存,何时释放内存。对象的生存期不是由定义该对象的范围决定的。我们通过运算符新和智能指针来完成。在现代 C++ 中,我们应该更喜欢智能指针工具而不是新操作符。
21.6 静态储存持续时间
当一个对象声明被加上一个static描述符时,这意味着静态对象的存储在程序开始时被分配,在程序结束时被释放。这种对象只有一个实例,并且(除了少数例外)当程序结束时,它们的生命周期也就结束了。它们是我们可以在程序执行的任何时候访问的对象。我们将在本书的后面讨论静态描述符和静态初始化。
21.7 运算符新增和删除
我们可以动态地为我们的对象分配和释放存储,并让指针指向这个新分配的内存。
操作符new为一个对象分配空间。对象被分配在自由存储上,通常称为堆或堆内存。必须使用操作符delete取消分配已分配的内存。它用一个操作符new释放先前分配的内存。示例:
#include <iostream>
int main()
{
int* p = new int;
*p = 123;
std::cout << "The pointed-to value is: " << *p;
delete p;
}
此示例在自由存储上为一个整数分配空间。指针p现在指向为我们的整数新分配的内存。我们现在可以通过取消引用一个指针来给新分配的 integer 对象赋值。最后,我们通过调用操作符delete来释放内存。
如果我们想为一个数组分配内存,我们使用操作符 new[]。为了释放分配给数组的内存,我们使用操作符delete[]。指针和数组是相似的,经常可以互换使用。指针可以被下标操作符[]取消引用。示例:
#include <iostream>
int main()
{
int* p = new int[3];
p[0] = 1;
p[1] = 2;
p[2] = 3;
std::cout << "The values are: " << p[0] << ' ' << p[1] << ' ' << p[2];
delete[] p;
}
这个例子为三个整数分配空间,一个三个整数的数组使用操作符new[].我们的指针p现在指向数组中的第一个元素。然后,使用下标操作符[],我们取消引用并给每个数组元素赋值。最后,我们使用操作符delete[]释放内存。记住:永远是你new选择的delete,永远是你new[]选择的delete[]。
记住:比起操作符new,更喜欢智能指针。在自由存储上分配的对象的生存期不受定义对象的作用域的限制。我们手动为对象分配和释放内存,从而控制对象何时被创建,何时被销毁。
二十二、练习
22.1 自动存储持续时间
编写一个程序,在main函数范围内定义两个类型为int的变量,并自动存储持续时间(放在栈上)。
#include <iostream>
int main()
{
int x = 123;
int y = 456;
std::cout << "The values with automatic storage durations are: " << x << " and " << y;
}
22.2 动态存储持续时间
编写一个程序,定义一个类型为int*的变量,该变量指向一个具有动态存储持续时间的对象(放在堆上) :
#include <iostream>
int main()
{
int* p = new int{ 123 };
std::cout << "The value with a dynamic storage duration is: " << *p;
delete p;
}
解释
在这个例子中,对象p只指向具有动态存储持续时间的对象。p对象本身有一个自动存储持续时间。要删除堆上的对象,我们需要使用删除操作符。
22.3 自动和动态存储持续时间
编写一个程序,定义一个名为 x 的类型为int的变量,自动存储持续时间,以及一个指向具有动态存储持续时间的对象的类型为 int*的变量。两个变量在同一范围内:
#include <iostream>
int main()
{
int x = 123; // automatic storage duration
std::cout << "The value with an automatic storage duration is: " << x << '\n';
int* p = new int{ x }; // allocate memory and copy the value from x to it
std::cout << "The value with a dynamic storage duration is: " << *p << '\n';
delete p;
} // end of scope here
二十三、类——简介
类是用户定义的类型。一个类由成员组成。成员是数据成员和成员函数。一个类可以被描述为数据和数据上的一些功能,打包成一个。一个类的实例称为对象。为了只声明一个类名,我们写:
class MyClass;
为了定义一个空类,我们添加了一个用大括号{}标记的类体:
class MyClass{};
为了创建一个类的实例,一个对象,我们使用:
class MyClass{};
int main()
{
MyClass o;
}
解释
我们定义了一个名为MyClass的类。然后我们创建了一个类型为MyClass的对象 o。据说o是一个对象,一个类实例。
23.1 数据成员字段
一个类可以包含一组数据。这些被称为成员字段。让我们向我们的类添加一个成员字段,并使其类型为char:
class MyClass
{
char c;
};
现在我们的类有了一个名为c的char类型的数据成员字段。现在让我们再添加两个类型为int和double的字段:
class MyClass
{
char c;
int x;
double d;
};
现在我们的类有三个成员字段,每个成员字段都有自己的名字。
23.2 成员功能
类似地,一个类可以存储函数。这些被称为成员函数。它们主要用于对数据字段执行一些操作。要声明一个名为dosomething()的 void 类型的成员函数,我们编写:
class MyClass
{
void dosomething();
};
有两种方法来定义这个成员函数。第一种是在类内部定义它:
class MyClass
{
void dosomething()
{
std::cout << "Hello World from a class.";
}
};
第二个是在类外定义。在这种情况下,我们首先编写函数类型,然后是类名,接着是 scope resolution :: operator,然后是函数名、参数列表(如果有)和函数体:
class MyClass
{
void dosomething();
};
void MyClass::dosomething()
{
std::cout << "Hello World from a class.";
}
这里,我们在类内部声明了一个成员函数,并在类外部定义了它。
一个类中可以有多个成员函数。为了在一个类中定义它们,我们应该这样写:
class MyClass
{
void dosomething()
{
std::cout << "Hello World from a class.";
}
void dosomethingelse()
{
std::cout << "Hello Universe from a class.";
}
};
要在类内声明成员函数并在类外定义它们,我们应该写:
class MyClass
{
void dosomething();
void dosomethingelse();
};
void MyClass::dosomething()
{
std::cout << "Hello World from a class.";
}
void MyClass::dosomethingelse()
{
std::cout << "Hello Universe from a class.";
}
现在我们可以创建一个既有数据成员字段又有成员函数的简单类:
class MyClass
{
int x;
void printx()
{
std::cout << "The value of x is:" << x;
}
};
这个类有一个名为x,的int类型的数据字段,还有一个名为printx()的成员函数。这个成员函数读取 x 的值并打印出来。这个例子是对成员访问描述符或类成员可见性的介绍。
23.3 访问描述符
如果有一种方法可以禁止访问成员字段,但允许访问对象的成员函数和其他访问类成员的实体,这不是很方便吗?这就是访问描述符的用途。它们为类成员指定访问权限。有三种访问描述符/标签:公共、受保护和私有:
class MyClass
{
public:
// everything in here
// has public access level
protected:
// everything in here
// has protected access level
private:
// everything in here
// has private access level
};
如果没有访问描述符,则类的默认可见性/访问描述符是private:
class MyClass
{
// everything in here
// has private access by default
};
另一种写类的方法是写一个struct。一个结构也是一个class,默认情况下成员拥有public访问权限。因此,struct与class是一回事,但默认情况下带有一个public访问描述符:
struct MyStruct
{
// everything in here
// is public by default
};
现在,我们将只关注public和private访问描述符。公共访问成员可以在任何地方访问。例如,其他类成员和我们类的对象都可以访问它们。为了从一个对象中访问一个类成员,我们使用点。运算符。
让我们定义一个类,其中所有成员都有公共访问权。要用公共访问描述符定义一个类,我们可以写:
class MyClass
{
public:
int x;
void printx()
{
std::cout << "The value of x is:" << x;
}
};
让我们实例化这个类并在我们的主程序中使用它:
#include <iostream>
class MyClass
{
public:
int x;
void printx()
{
std::cout << "The value of data member x is: " << x;
}
};
int main()
{
MyClass o;
o.x = 123; // x is accessible to object o
o.printx(); // printx() is accessible to object o
}
我们的对象o现在可以直接访问所有成员字段,因为它们都被标记为 public。无论访问描述符是什么,成员字段总是可以相互访问。这就是为什么成员函数printx()可以访问成员字段x并打印或更改其值。
私有访问成员只能被其他类成员访问,而不能被对象访问。附有完整注释的示例:
#include <iostream>
class MyClass
{
private:
int x; // x now has private access
public:
void printx()
{
std::cout << "The value of x is:" << x; // x is accessible to // printx()
}
};
int main()
{
MyClass o; // Create an object
o.x = 123; // Error, x has private access and is not accessible to // object o
o.printx(); // printx() is accessible from object o
}
我们的对象o现在只能访问类的公共部分中的成员函数printx()。它不能访问类的私有部分中的成员。
如果我们希望类成员可以被我们的对象访问,那么我们将把它们放在public:区域内。如果我们不希望类成员被我们的对象访问,那么我们将把它们放入private:区域。
我们希望数据成员拥有私有访问权限,而函数成员拥有公共访问权限。这样,我们的对象可以直接访问成员函数,但不能访问成员字段。还有另一个访问描述符叫做protected:,我们将在本书后面学习继承时讨论它。
23.4 施工人员
构造器是与类同名的成员函数。为了初始化一个类的对象,我们使用构造器。构造器的目的是初始化一个类的对象。它构造一个对象,并可以为数据成员设置值。如果一个类有一个构造器,那么该类的所有对象都将被一个构造器调用初始化。
默认构造器
没有参数或者设置了默认参数的构造器称为默认构造器。它是一个可以不带参数调用的构造器:
#include <iostream>
class MyClass
{
public:
MyClass()
{
std::cout << "Default constructor invoked." << '\n';
}
};
int main()
{
MyClass o; // invoke a default constructor
}
默认构造器的另一个例子是带有默认参数的构造器:
#include <iostream>
class MyClass
{
public:
MyClass(int x = 123, int y = 456)
{
std::cout << "Default constructor invoked." << '\n';
}
};
int main()
{
MyClass o; // invoke a default constructor
}
如果代码中没有显式定义默认构造器,编译器将生成默认构造器。但是当我们定义一个我们自己的构造器,一个需要参数的构造器时,默认的构造器被移除,并且不是由编译器生成的。
对象初始化时调用构造器。它们不能被直接调用。
构造器可以有任意参数;在这种情况下,我们可以称它们为用户提供的构造器:
#include <iostream>
class MyClass
{
public:
int x, y;
MyClass(int xx, int yy)
{
x = xx;
y = yy;
}
};
int main()
{
MyClass o{ 1, 2 }; // invoke a user-provided constructor
std::cout << "User-provided constructor invoked." << '\n';
std::cout << o.x << ' ' << o.y;
}
在这个例子中,我们的类有两个类型为int的数据字段和一个构造器。构造器接受两个参数,并将它们赋给数据成员。我们通过用MyClass o{ 1, 2 };在初始化列表中提供参数来调用构造器
构造器没有返回类型,它们的目的是初始化其类的对象。
23.4.2 成员初始化
在前面的例子中,我们使用了一个构造器体和赋值来给每个类成员赋值。一个更好、更有效的初始化类对象的方法是在构造器的定义中使用构造器的成员初始化列表:
#include <iostream>
class MyClass
{
public:
int x, y;
MyClass(int xx, int yy)
: x{ xx }, y{ yy } // member initializer list
{
}
};
int main()
{
MyClass o{ 1, 2 }; // invoke a user-defined constructor
std::cout << o.x << ' ' << o.y;
}
成员初始值设定项列表以冒号开头,后面是成员名及其初始值设定项,其中每个初始化表达式用逗号分隔。这是初始化类数据成员的首选方式。
复制构造器
当我们用同一个类的另一个对象初始化一个对象时,我们调用一个复制构造器。如果我们不提供我们的复制构造器,编译器会生成一个默认的复制构造器来执行所谓的浅层复制。示例:
#include <iostream>
class MyClass
{
private:
int x, y;
public:
MyClass(int xx, int yy) : x{ xx }, y{ yy }
{
}
};
int main()
{
MyClass o1{ 1, 2 };
MyClass o2 = o1; // default copy constructor invoked
}
在这个例子中,我们用相同类型的对象o1初始化对象o2。这将调用默认的复制构造器。
我们可以提供自己的复制构造器。复制构造器有一个特殊的参数签名MyClass(const MyClass& rhs).用户定义的复制构造器示例:
#include <iostream>
class MyClass
{
private:
int x, y;
public:
MyClass(int xx, int yy) : x{ xx }, y{ yy }
{
}
// user defined copy constructor
MyClass(const MyClass& rhs)
: x{ rhs.x }, y{ rhs.y } // initialize members with other object's // members
{
std::cout << "User defined copy constructor invoked.";
}
};
int main()
{
MyClass o1{ 1, 2 };
MyClass o2 = o1; // user defined copy constructor invoked
}
在这里,我们定义了自己的复制构造器,在该构造器中,我们用其他对象的数据成员显式初始化数据成员,并在控制台/标准输出中打印出一条简单的消息。
请注意,默认的复制构造器不能正确地复制某些类型的成员,比如指针、数组等。为了正确地制作副本,我们需要在复制构造器中定义自己的复制逻辑。这被称为深度复制。例如,对于指针,我们需要创建一个指针,并在我们的用户定义的复制构造器中为它所指向的对象赋值:
#include <iostream>
class MyClass
{
private:
int x;
int* p;
public:
MyClass(int xx, int pp)
: x{ xx }, p{ new int{pp} }
{
}
MyClass(const MyClass& rhs)
: x{ rhs.x }, p{ new int {*rhs.p} }
{
std::cout << "User defined copy constructor invoked.";
}
};
int main()
{
MyClass o1{ 1, 2 };
MyClass o2 = o1; // user defined copy constructor invoked
}
这里我们有两个构造器,一个是用户提供的常规构造器,另一个是用户自定义的复制构造器。第一个构造器初始化一个对象,并在这里调用:main函数中的MyClass o1{ 1, 2 };。
第二,用户定义的复制构造器在这里被调用:MyClass o2 = o1;这个构造器现在正确地复制了来自int和int*成员字段的值。
在这个例子中,我们将指针作为成员字段。如果我们忽略了用户定义的复制构造器,而依赖于默认的复制构造器,那么只有int成员字段会被正确地复制,而指针不会。在本例中,我们对此进行了纠正。
除了复制,还有一个移动语义,数据从一个对象移动到另一个对象。这个语义通过一个移动构造器和一个移动赋值操作符来表示。
23.4.4 复制转让
到目前为止,我们已经使用复制构造器用一个对象初始化另一个对象。我们也可以在初始化/创建对象后将值复制到对象中。为此,我们使用了一个拷贝赋值。简单地说,当我们在同一行使用=操作符用另一个对象初始化一个对象时,复制操作使用复制构造器:
MyClass copyfrom;
MyClass copyto = copyfrom; // on the same line, uses a copy constructor
当在一行上创建一个对象,然后将其分配给下一行时,它使用复制分配操作符从另一个对象复制数据:
MyClass copyfrom;
MyClass copyto;
copyto = copyfrom; // uses a copy assignment operator
复制赋值运算符具有以下签名:
MyClass& operator=(const MyClass& rhs)
要在类中定义用户定义的复制赋值操作符,我们使用:
class MyClass
{
public:
MyClass& operator=(const MyClass& rhs)
{
// implement the copy logic here
return *this;
}
};
注意重载的=操作符必须在末尾返回一个解引用的 this 指针。为了在类外定义一个用户定义的复制赋值操作符,我们使用:
class MyClass
{
public:
MyClass& operator=(const MyClass& rhs);
};
MyClass& MyClass::operator=(const MyClass& rhs)
{
// implement the copy logic here
return *this;
}
类似地,还有一个移动赋值操作符,我们将在本书后面讨论。在接下来的章节中会有更多关于操作符重载的内容。
移动构造器
除了复制,我们还可以将数据从一个对象移动到另一个对象。我们称之为移动语义。移动语义是通过移动构造器和移动赋值操作符实现的。从中移动数据的对象处于某种有效但未指定的状态。就执行速度而言,移动操作是高效的,因为我们不必制作副本。
Move 构造器接受名为的右值引用作为参数。
每个表达式都可以在赋值操作符的左边或右边找到自己。可以在左边使用的表达式称为左值,如变量、函数调用、类成员等。可以在赋值运算符右侧使用的表达式称为右值,如文字和其他表达式。
现在,move 语义接受对该右值的引用。右值引用类型的签名是带有双引用符号的T&、。因此,移动构造器的签名是:
MyClass (MyClass&& rhs)
为了将某些内容转换为右值引用,我们使用了 std::move 函数。这个函数将对象转换为一个右值引用。它不会移动任何东西。调用移动构造器的示例:
#include <iostream>
class MyClass { };
int main()
{
MyClass o1;
MyClass o2 = std::move(o1);
std::cout << "Move constructor invoked.";
// or MyClass o2{std::move(o1)};
}
在这个例子中,我们定义了一个名为o1的MyClass类型的对象。然后我们初始化第二个对象 o2,将对象o1中的所有内容移动到o2.中。为此,我们需要将o2转换为带有std::move(o1)的右值引用。这又调用了o2的MyClass移动构造器。
如果用户不提供移动构造器,编译器会提供隐式生成的默认移动构造器。
让我们指定我们自己的、用户定义的移动构造器:
#include <iostream>
#include <string>
class MyClass
{
private:
int x;
std::string s;
public:
MyClass(int xx, std::string ss) // user provided constructor
: x{ xx }, s{ ss }
{}
MyClass(MyClass&& rhs) // move constructor
:
x{ std::move(rhs.x) }, s{ std::move(rhs.s) }
{
std::cout << "Move constructor invoked." << '\n';
}
};
int main()
{
MyClass o1{ 1, "Some string value" };
MyClass o2 = std::move(o1);
}
此示例定义了一个具有两个数据成员和两个构造器的类。第一个构造器是一些用户提供的构造器,用于用提供的参数初始化数据成员。
第二个构造器是用户定义的 move 构造器,它接受一个名为rhs的类型为MyClass&&的右值引用参数。这个参数将成为我们的std::move(o1)参数/对象。然后在构造器初始化列表中,我们也使用std::move函数将数据字段从o1移动到o2。
移动分配
当我们声明一个对象,然后试图给它赋值一个右值引用时,调用移动赋值操作符。这是通过移动分配运算符完成的。移动赋值操作符的签名是:MyClass& operator=(MyClass&& otherobject)。
要在类中定义用户定义的移动赋值操作符,我们使用:
class MyClass
{
public:
MyClass& operator=(MyClass&& otherobject)
{
// implement the copy logic here
return *this;
}
};
与任何赋值操作符重载一样,我们必须在最后返回一个解引用的 this 指针。为了在类外定义一个移动赋值操作符,我们使用:
class MyClass
{
public:
MyClass& operator=(const MyClass& rhs);
};
MyClass& MyClass::operator=(const MyClass& rhs)
{
// implement the copy logic here
return *this;
}
改编自移动构造器示例的移动赋值运算符示例如下:
#include <iostream>
#include <string>
class MyClass
{
private:
int x;
std::string s;
public:
MyClass(int xx, std::string ss) // user provided constructor
: x{ xx }, s{ ss }
{}
MyClass& operator=(MyClass&& otherobject) // move assignment operator
{
x = std::move(otherobject.x);
s = std::move(otherobject.s);
return *this;
}
};
int main()
{
MyClass o1{ 123, "This is currently in object 1." };
MyClass o2{ 456, "This is currently in object 2." };
o2 = std::move(o1); // move assignment operator invoked
std::cout << "Move assignment operator used.";
}
这里我们定义了两个对象,分别叫做o1和o2。然后我们通过使用std::move(o1)表达式给对象o2分配一个(对象o1的)右值引用,试图将数据从对象o1移动到o2。这调用了我们的对象 o2 中的移动赋值操作符。移动赋值操作符实现本身使用std::move()函数将每个数据成员转换为一个右值引用。
运算符超载
类的对象可以在表达式中作为操作数使用。例如,我们可以这样做:
myobject = otherobject;
myobject + otherobject;
myobject / otherobject;
myobject++;
++myobject;
这里一个类的对象被用作操作数。为此,我们需要重载复杂类型(如类)的操作符。据说我们需要重载它们来提供对一个类的对象的有意义的操作。有些运算符可以为类重载;有些不能。我们可以重载以下运算符:
算术运算符、二元运算符、布尔运算符、一元运算符、比较运算符、复合运算符、函数和下标运算符:
+ - * / % ^ & | ~ ! = < > == != <= >= += -= *= /= %= ^= &= |= << >> >>= <<= && || ++ -- , ->* -> () []
当重载类时,每个操作符都带有自己的签名和规则集。有些运算符重载是作为成员函数实现的,有些是作为非成员函数实现的。让我们为类重载一元前缀++ 操作符。它的签名是:
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass()
: x{ 0 }, d{ 0.0 }
{
}
// prefix operator ++
MyClass& operator++()
{
++x;
++d;
std::cout << "Prefix operator ++ invoked." << '\n';
return *this;
}
};
int main()
{
MyClass myobject;
// prefix operator
++myobject;
// the same as:
myobject.operator++();
}
在这个例子中,当在我们的类中调用时,重载的前缀 increment ++ 操作符将每个成员字段递增 1。我们也可以通过调用一个.operator actual_operator_name ( parameters_if_any );来调用一个操作符,比如.operator++();
通常运算符是相互依赖的,并且可以根据其他运算符来实现。为了实现后缀运算符 ++ ,我们将根据前缀运算符来实现它:
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass()
: x{ 0 }, d{ 0.0 }
{
}
// prefix operator ++
MyClass& operator++()
{
++x;
++d;
std::cout << "Prefix operator ++ invoked." << '\n';
return *this;
}
// postfix operator ++
MyClass operator++(int)
{
MyClass tmp(*this); // create a copy
operator++(); // invoke the prefix operator overload
std::cout << "Postfix operator ++ invoked." << '\n';
return tmp; // return old value
}
};
int main()
{
MyClass myobject;
// postfix operator
myobject++;
// is the same as if we had:
myobject.operator++(0);
}
请不要过于担心操作符重载的有些不一致的规则。记住,每个(一组)操作符都有自己的重载规则。
让我们来霸王一个二元运算符 += :
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{
}
MyClass& operator+=(const MyClass& rhs)
{
this->x += rhs.x;
this->d += rhs.d;
return *this;
}
};
int main()
{
MyClass myobject{ 1, 1.0 };
MyClass mysecondobject{ 2, 2.0 };
myobject += mysecondobject;
std::cout << "Used the overloaded += operator.";
}
现在,myobject成员字段x的值为 3,成员字段d的值为 3.0。
让我们根据 += 运算符实现算术 + 运算符:
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{
}
MyClass& operator+=(const MyClass& rhs)
{
this->x += rhs.x;
this->d += rhs.d;
return *this;
}
friend MyClass operator+(MyClass lhs, const MyClass& rhs)
{
lhs += rhs;
return lhs;
}
};
int main()
{
MyClass myobject{ 1, 1.0 };
MyClass mysecondobject{ 2, 2.0 };
MyClass myresult = myobject + mysecondobject;
std::cout << "Used the overloaded + operator.";
}
总结:
当我们需要对一个类的对象执行算术、逻辑和其他操作时,我们需要重载适当的操作符。重载每个操作符都有规则和签名。某些运算符可以根据其他运算符来实现。关于运算符重载规则的完整列表,请参考位于 https://en.cppreference.com/w/cpp/language/operators 的 C++ 参考。
23.6 解构器
正如我们前面看到的,构造器是一个成员函数,当对象初始化时被调用。类似地,析构函数是一个在对象被销毁时被调用的成员函数。析构函数的名称是波浪号~后跟一个类名:
class MyClass
{
public:
MyClass() {} // constructor
~MyClass() {} // destructor
};
析构函数不带参数,每个类只有一个析构函数。示例:
#include <iostream>
class MyClass
{
public:
MyClass() {} // constructor
~MyClass()
{
std::cout << "Destructor invoked.";
} // destructor
};
int main()
{
MyClass o;
} // destructor invoked here, when o gets out of scope
当一个对象超出范围或者一个指向对象的指针被删除时,析构函数被调用。我们不应该直接调用析构函数。
析构函数可以用来清理被占用的资源。示例:
#include <iostream>
class MyClass
{
private:
int* p;
public:
MyClass()
: p{ new int{123} }
{
std::cout << "Created a pointer in the constructor." << '\n';
}
~MyClass()
{
delete p;
std::cout << "Deleted a pointer in the destructor." << '\n';
}
};
int main()
{
MyClass o; // constructor invoked here
} // destructor invoked here
这里我们在构造器中为指针分配内存,在析构函数中释放内存。这种类型的资源分配/解除分配被称为 RAII,或者资源获取是初始化。不应直接调用析构函数。
Important
new和delete的使用,以及现代 C++ 中原始指针的使用,不鼓励。我们应该使用智能指针来代替。我们将在本书的后面讨论它们。让我们为这节课的介绍部分做一些练习。*
二十四、练习
24.1 类实例
编写一个程序,定义一个名为 MyClass 的空类,并在主函数中创建一个 MyClass 的实例。
class MyClass
{
};
int main()
{
MyClass o;
}
24.2 具有数据成员的类
编写一个程序,定义一个名为 MyClass 的类,该类有三个类型为char、int,和bool的数据成员。在主函数中创建该类的一个实例。
class MyClass
{
char c;
int x;
bool b;
};
int main()
{
MyClass o;
}
24.3 具有成员函数的类
编写一个程序,用一个名为printmessage()的成员函数定义一个名为MyClass的类。在类中定义printmessage()成员函数,并让它输出“Hello World”字符串。创建该类的一个实例,并使用该对象调用该类的成员函数。
#include <iostream>
class MyClass
{
public:
void printmessage()
{
std::cout << "Hello World.";
}
};
int main()
{
MyClass o;
o.printmessage();
}
24.4 具有数据和函数成员的类
编写一个程序,用一个名为printmessage()的成员函数定义一个名为MyClass的类。在类外定义printmessage()成员函数,并让它输出"Hello World."字符串。创建该类的一个实例,并使用该对象调用成员函数。
#include <iostream>
class MyClass
{
public:
void printmessage();
};
void MyClass::printmessage()
{
std::cout << "Hello World.";
}
int main()
{
MyClass o;
o.printmessage();
}
24.5 类访问描述符
编写一个程序,用一个名为 x 的类型为int的私有数据成员和两个成员函数定义一个名为MyClass的类。名为setx(int myvalue)的第一个成员函数将把 x 的值设置为其参数myvalue。第二个成员函数名为getx(),类型为int,返回值为 x 。创建类的实例,并使用对象来访问这两个成员函数。
#include <iostream>
class MyClass
{
private:
int x;
public:
void setx(int myvalue)
{
x = myvalue;
}
int getx()
{
return x;
}
};
int main()
{
MyClass o;
o.setx(123);
std::cout << "The value of x is: " << o.getx();
}
24.6 用户定义的默认构造器和析构函数
编写一个程序,用用户定义的默认构造器和析构函数定义一个名为MyClass的类。在类外定义构造器和析构函数。两个成员函数都将在标准输出上输出一个自由选择的文本。在函数 main 中创建一个类的对象。
#include <iostream>
class MyClass
{
public:
MyClass();
~MyClass();
};
MyClass::MyClass()
{
std::cout << "Constructor invoked." << '\n';
}
MyClass::~MyClass()
{
std::cout << "Destructor invoked." << '\n';
}
int main()
{
MyClass o;
}
24.7 构造器初始化列表
编写一个程序,定义一个名为MyClass,的类,它有两个类型为int和double的私有数据成员。在类外部,定义一个用户提供的接受两个参数的构造器。构造器使用初始值设定项用参数初始化两个数据成员。在类外部,定义一个名为printdata()的函数,它打印两个数据成员的值。
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd);
void printdata();
};
MyClass::MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{
}
void MyClass::printdata()
{
std::cout << " The value of x: " << x << ", the value of d: " << d << '\n';
}
int main()
{
MyClass o{ 123, 456.789 };
o.printdata();
}
24.8 用户定义的复制构造器
编写一个程序,用任意数据字段定义一个名为MyClass的类。使用初始化数据成员的参数编写用户定义的构造器。编写一个用户定义的复制构造器来复制所有成员。创建一个名为 o1 的类的对象,并用值初始化它。创建一个名为 o2 的类的另一个对象,并用对象 o 初始化它。
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd); // user-provided constructor
MyClass(const MyClass& rhs); // user-defined copy constructor
void printdata();
};
MyClass::MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{}
MyClass::MyClass(const MyClass& rhs)
: x{ rhs.x }, d{ rhs.d }
{}
void MyClass::printdata()
{
std::cout << "X is: " << x << ", d is: " << d << '\n';
}
int main()
{
MyClass o1{ 123, 456.789 }; // invokes a user-provided constructor
MyClass o2 = o1; // invokes a user-defined copy constructor
o1.printdata();
o2.printdata();
}
24.9 用户定义的移动构造器
编写一个程序,用两个数据成员定义一个类,一个用户提供的构造器,一个用户提供的移动构造器和一个打印数据的成员函数。在主程序中调用 move 构造器。打印移动到的对象数据字段。
#include <iostream>
#include <string>
class MyClass
{
private:
double d;
std::string s;
public:
MyClass(double dd, std::string ss) // user-provided constructor
: d{ dd }, s{ ss }
{}
MyClass(MyClass&& otherobject) // user-defined move constructor
:
d{ std::move(otherobject.d) }, s{ std::move(otherobject.s) }
{
std::cout << "Move constructor invoked." << '\n';
}
void printdata()
{
std::cout << "The value of doble is: " << d << ", the value of string is: " << s << '\n';
}
};
int main()
{
MyClass o1{ 3.14, "This was in object 1" };
MyClass o2 = std::move(o1); // invokes the move constructor
o2.printdata();
}
24.10 重载算术运算符
写一个重载算术运算符的程序——用一个复合算术运算符-=。打印出结果对象成员字段的值。
#include <iostream>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{
}
void printvalues()
{
std::cout << "The values of x is: " << x << ", the value of d is: " << d;
}
MyClass& operator-=(const MyClass& rhs)
{
this->x -= rhs.x;
this->d -= rhs.d;
return *this;
}
friend MyClass operator-(MyClass lhs, const MyClass& rhs)
{
lhs -= rhs;
return lhs;
}
};
int main()
{
MyClass myobject{ 3, 3.0 };
MyClass mysecondobject{ 1, 1.0 };
MyClass myresult = myobject - mysecondobject;
myresult.printvalues();
}