面向零基础初学者的现代 C++ 教程(三)
二十五、类——继承和多态
在这一章中,我们将讨论一些面向对象编程的基本构件,比如继承和多态。
25.1 继承
我们可以从现有的类构建一个类。据说一个类可以从一个已有的类派生。这被称为继承,是面向对象编程的支柱之一,缩写为 OOP。为了从现有的类中派生出一个类,我们编写:
class MyDerivedClass : public MyBaseClass {};
一个简单的例子是:
class MyBaseClass
{
};
class MyDerivedClass : public MyBaseClass
{
};
int main()
{
}
在这个例子中,MyDerivedClass 继承了MyBaseClass。
让我们把术语抛开。据说MyDerivedClass 是从MyBaseClass派生出,或者说MyBaseClass是MyDerivedClass的基类。也有人说MyDerivedClass 就是 ??。它们的意思都一样。
现在这两个阶层有了某种关系*。这种关系可以通过不同的命名约定来表达,但是最重要的一个是继承。派生类和派生类的对象可以访问基类的public成员:*
class MyBaseClass
{
public:
char c;
int x;
};
class MyDerivedClass : public MyBaseClass
{
// c and x also accessible here
};
int main()
{
MyDerivedClass o;
o.c = 'a';
o.x = 123;
}
以下示例引入了名为protected:的新访问描述符。派生类本身可以访问基类的protected成员。protected访问描述符允许访问基类和派生类,但不允许访问对象:
class MyBaseClass
{
protected:
char c;
int x;
};
class MyDerivedClass : public MyBaseClass
{
// c and x also accessible here
};
int main()
{
MyDerivedClass o;
o.c = 'a'; // Error, not accessible to object
o.x = 123; // error, not accessible to object
}
派生类无法访问基类的private成员:
class MyBaseClass
{
private:
char c;
int x;
};
class MyDerivedClass : public MyBaseClass
{
// c and x NOT accessible here
};
int main()
{
MyDerivedClass o;
o.c = 'a'; // Error, not accessible to object
o.x = 123; // error, not accessible to object
}
派生类继承基类的公共和受保护成员,并且可以引入自己的成员。一个简单的例子:
class MyBaseClass
{
public:
char c;
int x;
};
class MyDerivedClass : public MyBaseClass
{
public:
double d;
};
int main()
{
MyDerivedClass o;
o.c = 'a';
o.x = 123;
o.d = 456.789;
}
这里我们从MyBaseClass类继承了一切,并在MyDerivedClass中引入了一个新的成员字段,名为d。所以,有了MyDerivedClass,我们正在扩展MyBaseClass的能力。字段d仅存在于MyDerivedClass中,并且可被派生类及其对象访问。对于MyBaseClass类,它是不可访问的,因为它不存在于那里。
请注意,还有其他继承类的方法,比如通过受保护的和私有的继承,但是像class MyDerivedClass : public MyBaseClass这样的公共继承是最广泛使用的,我们现在将坚持使用它。
派生类本身可以是基类。示例:
class MyBaseClass
{
public:
char c;
int x;
};
class MyDerivedClass : public MyBaseClass
{
public:
double d;
};
class MySecondDerivedClass : public MyDerivedClass
{
public:
bool b;
};
int main()
{
MySecondDerivedClass o;
o.c = 'a';
o.x = 123;
o.d = 456.789;
o.b = true;
}
现在我们的类拥有了MyDerivedClass所拥有的一切,这包括了MyBaseClass所拥有的一切,外加一个额外的bool字段。据说继承产生了一个特殊的层次类。
当我们想要扩展类的功能时,这种方法被广泛使用。
派生类与基类兼容。指向派生类的指针与指向基类的指针兼容。这允许我们利用多态性,,我们将在下一章讨论。
25.2 多态性
据说派生类是一个基类。它的类型与基类类型兼容。此外,指向派生类的指针与指向基类的指针兼容。这很重要,所以让我们重复一下:指向派生类的指针与指向基类的指针是兼容的。与继承一起,这被用来实现被称为多态性的功能。多态性意味着对象可以变成不同的类型。C++ 中的多态性是通过一个称为虚函数的接口实现的。虚函数是其行为可以在后续派生类中被重写的函数。我们的指针/对象将变成不同的类型来调用适当的函数。示例:
#include <iostream>
class MyBaseClass
{
public:
virtual void dowork()
{
std::cout << "Hello from a base class." << '\n';
}
};
class MyDerivedClass : public MyBaseClass
{
public:
void dowork()
{
std::cout << "Hello from a derived class." << '\n';
}
};
int main()
{
MyBaseClass* o = new MyDerivedClass;
o->dowork();
delete o;
}
在这个例子中,我们有一个简单的继承,其中MyDerivedClass从MyBaseClass派生而来。
MyBaseClass类有一个名为dowork()的函数,带有一个virtual描述符。虚拟意味着该函数可以在后续的派生类中被重写/重定义,并且适当的版本将通过多态对象被调用。派生类中有一个同名的函数和相同类型的参数(在我们的例子中没有)。
在我们的主程序中,我们通过基类指针创建了一个MyDerivedClass类的实例。使用箭头操作符->我们调用函数的适当版本。这里,o 对象将变成不同的类型来调用适当的函数。这里它调用派生版本。这就是这个概念被称为多态性的原因。
如果派生类中没有dowork()函数,它将调用基类版本:
#include <iostream>
class MyBaseClass
{
public:
virtual void dowork()
{
std::cout << "Hello from a base class." << '\n';
}
};
class MyDerivedClass : public MyBaseClass
{
public:
};
int main()
{
MyBaseClass* o = new MyDerivedClass;
o->dowork();
delete o;
}
通过在函数声明的末尾指定= 0;,函数可以是纯虚拟的。纯虚函数没有定义,也叫接口。纯虚函数必须在派生类中重新定义。至少有一个纯虚函数的类被称为抽象类,不能被实例化。它们只能用作基类。示例:
#include <iostream>
class MyAbstractClass
{
public:
virtual void dowork() = 0;
};
class MyDerivedClass : public MyAbstractClass
{
public:
void dowork()
{
std::cout << "Hello from a derived class." << '\n';
}
};
int main()
{
MyAbstractClass* o = new MyDerivedClass;
o->dowork();
delete o;
}
需要补充的一点是,如果要在多态场景中使用基类,它必须有一个virtual析构函数。这确保了通过继承链适当地释放通过基类指针访问的对象:
class MyBaseClass
{
public:
virtual void dowork() = 0;
virtual ~MyBaseClass() {};
};
请记住,在现代 C++ 中,不鼓励使用运算符 new 和原始指针。我们应该使用智能指针。在这本书的后面会有更多的内容。
因此,面向对象编程的三个支柱是:
-
包装
-
继承
-
多态性
例如,封装就是将字段分组到不同的可见区域,对用户隐藏实现,并暴露接口。
继承是一种机制,我们可以通过从基类继承来创建类。继承创建了一定的类层次结构和它们之间的关系。
多态性是一种在运行时对象转变成不同类型的能力,确保调用正确的函数。这是通过继承、虚函数和重写函数以及基类和派生类指针来实现的。*
二十六、练习
26.1 继承
写一个程序,定义一个叫 Person 的基类。该类有以下成员:
-
名为名为的类型为 std::string 的数据成员
-
单参数,用户定义的构造器,初始化名
-
一个类型为 std::string 的 getter 函数,名为 getname(),,返回名字的值
然后,编写一个名为 Student、的类,它继承了类 Person 。班级学生有以下成员:
-
名为学期的整数数据成员
-
用户提供的构造器,用于初始化名称和学期字段
-
一个类型为 int 的 getter 函数,名为getsteam(),,返回学期的值
简而言之,我们将有一个基类 Person ,并在派生的 Student 类中扩展它的功能:
#include <iostream>
#include <string>
class Person
{
private:
std::string name;
public:
explicit Person(const std::string& aname)
: name{ aname }
{}
std::string getname() const { return name; }
};
class Student : public Person
{
private:
int semester;
public:
Student(const std::string& aname, int asemester)
: Person::Person{ aname }, semester{ asemester }
{}
int getsemester() const { return semester; }
};
int main()
{
Person person{ "John Doe." };
std::cout << person.getname() << '\n';
Student student{ "Jane Doe", 2 };
std::cout << student.getname() << '\n';
std::cout << "Semester is: " << student.getsemester() << '\n';
}
说明:我们有两个类,一个是基类(Person),一个(Student)是派生类。单参数构造器应该用explicit标记,以防止编译器进行隐式转换。人用户提供的单参数构造器就是这种情况:
explicit Person(const std::string& aname)
: name{ aname }
{}
不修改成员字段的成员函数应该标记为 const 。成员函数中的 const 修饰符保证函数不会修改数据成员,并且更易于编译器优化代码。两个 getname() 都是这种情况:
std::string getname() const { return name; }
和*get 学期()*成员函数:
int getsemester() const { return semester; }
学生类继承自人类和 ads 附加数据字段学期和成员函数get 学期()。Student类拥有基类所拥有的一切,并且通过添加新的字段扩展了基类的功能。学生的用户提供的构造器使用其初始化列表中的基类构造器来初始化名称字段:
Student(const std::string& aname, int asemester)
: Person::Person{ aname }, semester{ asemester }
{}
在 main()程序中,我们实例化了两个类:
Person person{ "John Doe." };
以及:
Student student{ "Jane Doe", 2 };
并调用它们的成员函数:
person.getname();
以及:
student.getname();
student.getsemester();
Important
在本书的后面,当我们讨论智能指针时,我们将做一个多态性练习。这是因为我们不想使用new和delete以及原始指针。
二十七、静态描述符
static描述符表示对象将有一个静态存储持续时间。静态对象的内存空间在程序启动时分配,在程序结束时释放。程序中只存在一个静态对象的实例。如果一个局部变量被标记为 static,那么它的空间在程序控件第一次遇到它的定义时被分配,当程序退出时被释放。
要在函数中定义局部静态变量,我们使用:
#include <iostream>
void myfunction()
{
static int x = 0; // defined only the first time, skipped every other // time
x++;
std::cout << x << '\n';
}
int main()
{
myfunction(); // x == 1
myfunction(); // x == 2
myfunction(); // x == 3
}
这个变量在程序第一次遇到这个函数时被初始化。该变量的值在函数调用中保持不变。这是什么意思?我们对其进行的最后一次更改保持不变。它不会在每次函数调用时都初始化为 0,只有在第一次调用时才会初始化。
这很方便,因为我们不必将值存储在某个全局变量 x 中。
我们可以定义静态类成员字段。静态类成员不是对象的一部分。它们独立于一个类的对象而存在。我们在类内部声明一个静态数据成员,在类外部只定义一次:
#include <iostream>
class MyClass
{
public:
static int x; // declare a static data member
};
int MyClass::x = 123; // define a static data member
int main()
{
MyClass::x = 456; // access a static data member
std::cout << "Static data member value is: " << MyClass::x;
}
这里我们声明了一个类中的静态数据成员。然后我们在类外定义了它。当在类外定义静态成员时,我们不需要使用静态描述符。然后,我们通过使用MyClass::data_member_name符号访问数据成员。
为了定义一个静态成员函数,我们在函数声明前添加了 static 关键字。类外的函数定义不使用静态关键字:
#include <iostream>
class MyClass
{
public:
static void myfunction(); // declare a static member function
};
// define a static member function
void MyClass::myfunction()
{
std::cout << "Hello World from a static member function.";
}
int main()
{
MyClass::myfunction(); // call a static member function
}
二十八、模板
模板是支持所谓的通用编程的机制。泛型广义上意味着我们可以定义一个函数或类,而不用担心它接受什么类型。
我们使用一些通用类型来定义这些函数和类。当我们实例化它们时,我们使用一个具体的类型。所以,当我们想要定义一个几乎可以接受任何类型的类或函数时,我们可以使用模板。
我们通过键入以下内容来定义模板:
template <typename T>
// the rest of our function or class code
这与我们使用:
template <class T>
// the rest of our function or class code
这里代表一个类型名。哪种类型?嗯,任何类型。这里 T 的意思是,对于所有类型的 T 。
让我们创建一个可以接受任何类型参数的函数:
#include <iostream>
template <typename T>
void myfunction(T param)
{
std::cout << "The value of a parameter is: " << param;
}
int main()
{
}
为了实例化一个函数模板,我们通过提供一个用尖括号括起来的特定类型名来调用一个函数:
#include <iostream>
template <typename T>
void myfunction(T param)
{
std::cout << "The value of a parameter is: " << param;
}
int main()
{
myfunction<int>(123);
myfunction<double>(123.456);
myfunction<char>('A');
}
我们可以把 T 看作一个特定类型的占位符,就是我们在实例化一个模板时提供的类型。因此,我们现在把我们的具体类型。整洁,哈?这样,我们可以对不同的类型使用相同的代码。
模板可以有多个参数。我们简单地列出模板参数,并用逗号分隔它们。接受两个模板参数的函数模板示例:
#include <iostream>
template <typename T, typename U>
void myfunction(T t, U u)
{
std::cout << "The first parameter is: " << t << '\n';
std::cout << "The second parameter is: " << u << '\n';
}
int main()
{
int x = 123;
double d = 456.789;
myfunction<int, double>(x, d);
}
为了定义一个类模板,我们使用:
#include <iostream>
template <typename T>
class MyClass {
private:
T x;
public:
MyClass(T xx)
:x{ xx }
{
}
T getvalue()
{
return x;
}
};
int main()
{
MyClass<int> o{ 123 };
std::cout << "The value of x is: " << o.getvalue() << '\n';
MyClass<double> o2{ 456.789 };
std::cout << "The value of x is: " << o2.getvalue() << '\n';
}
这里,我们定义了一个简单的类模板。这个类接受 t 类型。我们在类中任何合适的地方使用这些类型。在我们的主函数中,我们用具体的类型int和double实例化这些类。我们只需使用一个模板,而不必为两个或更多不同的类型编写相同的代码。
要在类外定义一个类模板成员函数,我们需要通过在成员函数定义前添加适当的模板声明来使它们成为模板。在这样的定义中,必须用模板参数调用类名。简单的例子:
#include <iostream>
template <typename T>
class MyClass {
private:
T x;
public:
MyClass(T xx);
};
template <typename T>
MyClass<T>::MyClass(T xx)
: x{xx}
{
std::cout << "Constructor invoked. The value of x is: " << x << '\n';
}
int main()
{
MyClass<int> o{ 123 };
MyClass<double> o2{ 456.789 };
}
让我们把它变得简单些。如果我们有一个只有一个 void 成员函数的类模板,我们可以写:
template <typename T>
class MyClass {
public:
void somefunction();
};
template <typename T>
void MyClass<T>::somefunction()
{
// the rest of the code
}
如果我们有一个带有 T 类型的单个成员函数的类模板,我们将使用:
template <typename T>
class MyClass {
public:
T genericfunction();
};
template <typename T>
T MyClass<T>::genericfunction()
{
// the rest of the code
}
现在,如果我们在一个类中有它们,并且我们想在类范围之外定义它们,我们将使用:
template <typename T>
class MyClass {
public:
void somefunction();
T genericfunction();
};
template <typename T>
void MyClass<T>::somefunction()
{
// the rest of the code
}
template <typename T>
T MyClass<T>::genericfunction()
{
// the rest of the code
}
模板专门化
如果我们希望我们的模板对于一个特定的类型有不同的行为,我们提供了所谓的模板专门化。如果参数是某种类型的,我们有时需要不同的代码。为此,我们在函数或类前加上:
template <>
// the rest of our code
为了将模板函数特殊化为类型 int ,我们编写:
#include <iostream>
template <typename T>
void myfunction(T arg)
{
std::cout << "The value of an argument is: " << arg << '\n';
}
template <>
// the rest of our code
void myfunction(int arg)
{
std::cout << "This is a specialization int. The value is: " << arg << '\n';
}
int main()
{
myfunction<char>('A');
myfunction<double>(345.678);
myfunction<int>(123); // invokes specialization
}
二十九、枚举
枚举,简称 enum ,是一种类型,其值是用户自定义的命名常量,称为枚举器。
有两种枚举:未划分范围的枚举和 ?? 范围的枚举。未划分的枚举类型可以用以下内容定义:
enum MyEnum
{
myfirstvalue,
mysecondvalue,
mythirdvalue
};
为了声明一个枚举类型的变量MyEnum,我们写:
enum MyEnum
{
myfirstvalue,
mysecondvalue,
mythirdvalue
};
int main()
{
MyEnum myenum = myfirstvalue;
myenum = mysecondvalue; // we can change the value of our enum object
}
每个枚举数都有一个基础类型的值。我们可以改变这些:
enum MyEnum
{
myfirstvalue = 10,
mysecondvalue,
mythirdvalue
};
这些未划分的枚举让它们的枚举器泄漏到一个外部作用域,在这个作用域中定义了枚举类型本身。旧枚举最好避免。比起这些老派的、未分类的枚举,我更喜欢范围内的枚举。限定范围的枚举不会将其枚举数泄漏到外部范围,也不能隐式转换为其他类型。为了定义一个限定了作用域的枚举,我们编写:
enum class MyEnum
{
myfirstvalue,
mysecondvalue,
mythirdvalue
};
要声明枚举类(作用域枚举)类型的变量,我们编写:
enum class MyEnum
{
myfirstvalue,
mysecondvalue,
mythirdvalue
};
int main()
{
MyEnum myenum = MyEnum::myfirstvalue;
}
为了访问枚举器值,我们在枚举器前面加上枚举名称和范围解析操作符::比如MyEnum::myfirstvalue, MyEnum:: mysecondvalue,等。
使用这些枚举,枚举数名称仅在枚举内部范围内定义,并隐式转换为基础类型。我们可以指定作用域枚举的基础类型:
enum class MyCharEnum : char
{
myfirstvalue,
mysecondvalue,
mythirdvalue
};
我们还可以通过指定值来更改枚举数的初始基础值:
enum class MyEnum
{
myfirstvalue = 15,
mysecondvalue,
mythirdvalue = 30
};
摘要:比起旧的简单的未划分的枚举,更喜欢枚举类枚举(限定了作用域的枚举)。当我们的对象需要一组预定义的命名值中的一个值时,使用枚举。
三十、练习
30.1 静态变量
写一个程序来检查一个函数被主程序调用了多少次。为此,我们将在函数中使用一个静态变量,该变量将在 main()中每次调用该函数时递增:
#include <iostream>
void myfunction()
{
static int counter = 0;
counter++;
std::cout << "The function is called " << counter << " time(s)." << '\n';
}
int main()
{
myfunction();
myfunction();
for (int i = 0; i < 5; i++)
{
myfunction();
}
}
30.2 静态数据成员
编写一个程序,用一个 std::string 类型的静态数据成员定义一个类。将数据成员公开。在类外定义静态数据成员。从 main()函数中更改静态数据成员值:
#include <iostream>
#include <string>
class MyClass
{
public:
static std::string name;
};
std::string MyClass::name = "John Doe";
int main()
{
std::cout << "Static data member value: " << MyClass::name << '\n';
MyClass::name = "Jane Doe";
std::cout << "Static data member value: " << MyClass::name << '\n';
}
30.3 静态成员函数
编写一个程序,用一个静态成员函数和一个常规成员函数定义一个类。公开这些函数。在类外定义这两个成员函数。在 main()中访问这两个函数:
#include <iostream>
#include <string>
class MyClass
{
public:
static void mystaticfunction();
void myfunction();
};
void MyClass::mystaticfunction()
{
std::cout << "Hello World from a static member function." << '\n';
}
void MyClass::myfunction()
{
std::cout << "Hello World from a regular member function." << '\n';
}
int main()
{
MyClass::mystaticfunction();
MyClass myobject;
myobject.myfunction();
}
30.4 功能模板
编写一个程序,为两个数相加的函数定义一个模板。数字具有相同的泛型类型 T,并作为参数传递给函数。使用 int 和 double 类型实例化 main()中的函数:
#include <iostream>
template <typename T>
T mysum(T x, T y)
{
return x + y;
}
int main()
{
int intresult = mysum<int>(10, 20);
std::cout << "The integer sum result is: " << intresult << '\n';
double doubleresult = mysum<double>(123.456, 789.101);
std::cout << "The double sum result is: " << doubleresult << '\n';
}
30.5 课程模板
编写一个程序,定义一个简单的类模板,该模板包含一个泛型数据成员、一个构造器、一个泛型 getter 函数和一个 setter 成员函数。在 main()函数中为 int 和 double 类型实例化一个类:
#include <iostream>
template <typename T>
class MyClass
{
private:
T x;
public:
MyClass(T xx)
: x{ xx }
{}
T getx() const
{
return x;
}
void setx(T ax)
{
x = ax;
}
};
int main()
{
MyClass<int> o{123};
std::cout << "The value of the data member is: " << o.getx() << '\n';
o.setx(456);
std::cout << "The value of the data member is: " << o.getx() << '\n';
MyClass<double> o2{ 4.25 };
std::cout << "The value of the data member is: " << o2.getx() << '\n';
o2.setx(6.28);
std::cout << "The value of the data member is: " << o2.getx() << '\n';
}
30.6 作用域枚举
编写一个程序,定义一个代表一周中各天的作用域枚举。创建该枚举的对象,为其赋值,检查该值是否为周一,如果是,将对象值更改为另一个枚举值:
#include <iostream>
enum class Days
{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
};
int main()
{
Days myday = Days::Monday;
std::cout << "The enum value is now Monday." << '\n';
if (myday == Days::Monday)
{
myday = Days::Friday;
}
std::cout << "Nobody likes Mondays. The value is now Friday.";
}
30.7 开关中的枚举
写一个定义枚举的程序。当在 switch 语句中使用枚举对象时,创建它。使用 switch 语句打印对象的值:
#include <iostream>
enum class Colors
{
Red,
Green,
Blue
};
int main()
{
Colors mycolors = Colors::Green;
switch (mycolors)
{
case Colors::Red:
std::cout << "The color is Red." << '\n';
break;
case Colors::Green:
std::cout << "The color is Green." << '\n';
break;
case Colors::Blue:
std::cout << "The color is Blue." << '\n';
break;
default:
break;
}
}
三十一、组织代码
我们可以将 C++ 代码分成多个文件。按照惯例,有两种类型的文件可以存储我们的 C++ 源代码:头文件(头文件)和源文件。
31.1 头文件和源文件
头文件是我们通常放置各种声明的源代码文件。头文件通常有*。h* (或者。 hpp 分机。源文件是我们可以存储定义和主程序的文件。他们通常有*。cpp* (或*)。cc* 扩展名。
然后我们使用#include预处理指令将头文件包含到源文件中。为了包含一个标准的库头文件,我们使用了#include语句,后跟一个不带扩展名的头文件名称,用尖括号<headername>括起来。示例:
#include <iostream>
#include <string>
// etc
为了包含用户定义的头文件,我们使用#include 语句,后跟一个完整的头文件名称,扩展名用双引号括起来。示例:
#include "myheader.h"
#include "otherheader.h"
// etc
现实情况是,有时我们需要同时包含标准库头和用户定义的头:
#include <iostream>
#include "myheader.h"
// etc
编译器将头文件和源文件中的代码缝合在一起,产生一个所谓的翻译单元。然后编译器使用这个文件创建一个目标文件。然后链接器将目标文件链接在一起,创建一个程序。
我们应该将声明和常量放在头文件中,将定义和可执行代码放在源文件中。
31.2 顶盖防护装置
多个源文件可能包含同一个头文件。为了确保我们的头在编译过程中只被包含一次,我们使用了一种叫做头保护的机制。它确保我们的头内容在编译过程中只包含一次。我们用以下宏将头文件中的代码括起来:
#ifndef MY_HEADER_H
#define MY_HEADER_H
// header file source code
// goes here
#endif
这种方法确保头文件中的代码在编译阶段只包含一次。
31.3 名称空间
到目前为止,我们已经看到了如何将 C++ 代码的各个部分分组到单独的文件中,这些文件被称为头文件和源文件。还有另一种方法可以对 C++ 的各个部分进行逻辑分组,那就是通过命名空间。命名空间是有名称的作用域。为了声明一个名称空间,我们编写:
namespace MyNameSpace
{
}
为了在名称空间中声明对象,我们使用:
namespace MyNameSpace
{
int x;
double d;
}
为了在名称空间之外引用这些对象,我们使用它们的完全限定名。这意味着我们使用名称空间名称::我们的对象符号。我们在声明对象的名称空间之外定义对象的示例:
namespace MyNameSpace
{
int x;
double d;
}
int main()
{
MyNameSpace::x = 123;
MyNameSpace::d = 456.789;
}
要将整个名称空间引入当前范围,我们可以使用using-指令:
namespace MyNameSpace
{
int x;
double d;
}
using namespace MyNameSpace;
int main()
{
x = 123;
d = 456.789;
}
如果我们的代码中有几个名称相同的独立名称空间,这意味着我们在扩展那个名称空间。示例:
namespace MyNameSpace
{
int x;
double d;
}
namespace MyNameSpace
{
char c;
bool b;
}
int main()
{
MyNameSpace::x = 123;
MyNameSpace::d = 456.789;
MyNameSpace::c = 'a';
MyNameSpace::b = true;
}
现在,在我们的MyNameSpace名称空间中有了 x、d、c 和 b。我们是在扩展的MyNameSpace,而不是重新定义它。
一个命名空间可以分布在多个文件中,包括头文件和源文件。我们经常会看到生产代码被包装到名称空间中。将代码按逻辑分组到名称空间是一种很好的机制。
两个不同名称的命名空间可以保存一个同名的对象。由于每个名称空间都是一个不同的作用域,它们现在用相同的名称声明了两个不同的不相关的对象。它可以防止名称冲突:
#include <iostream>
namespace MyNameSpace
{
int x;
}
namespace MySecondNameSpace
{
int x;
}
int main()
{
MyNameSpace::x = 123;
MySecondNameSpace::x = 456;
std::cout << "1st x: " << MyNameSpace::x << ", 2nd x: " << MySecondNameSpace::x;
}
三十二、练习
32.1 头文件和源文件
写一个在头文件中声明任意函数的程序。头文件叫做 myheader.h 在主程序源文件 source.cpp 里面定义这个函数。main函数也位于 source.cpp 文件中。将头文件包含到我们的源文件中并调用函数。
myheader.h:
void myfunction(); //function declaration
source.cpp:
#include "myheader.h" //include the header
#include <iostream>
int main()
{
myfunction();
}
// function definition
void myfunction()
{
std::cout << "Hello World from multiple files.";
}
32.2 多个源文件
写一个在头文件中声明任意函数的程序。头文件叫做 mylibrary.h 在源文件里面定义一个叫做 mylibrary.cpp 的函数。main函数位于第二个源文件 source.cpp 文件中。在两个源文件中包含头文件并调用函数。
mylibrary.h .:
void myfunction(); //function declaration
my library . CPP:my library .我的产品目录:
#include "mylibrary.h"
#include <iostream>
// function definition
void myfunction()
{
std::cout << "Hello World from multiple files.";
}
source.cpp:
#include "mylibrary.h"
int main()
{
myfunction();
}
说明:
这个程序有三个文件:
-
一个名为 mylibrary.h 的头文件,我们在其中放置了函数声明。
-
一个名为 mylibrary.cpp 的源文件,我们将函数定义放在这里。我们将头文件 mylibrary.h 包含到 mylibrary.cpp 源文件中。
-
主程序所在的名为 source.cpp 的源文件。我们还在这个源文件中包含了 mylibrary.h 头文件。
因为我们的头文件包含在多个源文件中,所以我们应该将头文件保护宏放入其中。 mylibrary.h 文件现在看起来像这样:
#ifndef MY_LIBRARY_H
#define MY_LIBRARY_H
void myfunction();
#endif // !MY_LIBRARY_H
用 g++ 编译一个有多个源文件的程序,我们使用:
g++ source.cpp mylibrary.cpp
Visual Studio IDE 自动处理多文件编译。
32.3 名称空间
编写一个程序,在命名空间内声明一个函数,在命名空间外定义该函数。调用主程序中的函数。命名空间和函数名是任意的。
#include <iostream>
namespace MyNameSpace
{
void myfunction();
}
void MyNameSpace::myfunction()
{
std::cout << "Hello World from a function inside a namespace.";
}
int main()
{
MyNameSpace::myfunction();
}
32.4 嵌套命名空间
编写一个程序,定义一个名为 A 的命名空间和嵌套在命名空间 A 中的另一个名为 B 的命名空间,在命名空间 B 中声明一个函数,并在两个命名空间之外定义该函数。调用主程序中的函数。然后,将整个名称空间 B 引入当前范围,并调用该函数。
#include <iostream>
namespace A
{
namespace B
{
void myfunction();
}
}
void A::B::myfunction()
{
std::cout << "Hello World from a function inside a nested namespace." << '\n';
}
int main()
{
A::B::myfunction();
using namespace A::B;
myfunction();
}
三十三、转换
类型可以转换为其他类型。例如,内置类型可以转换为其他内置类型。这里我们将讨论隐式和显式转换。
33.1 隐式转换
有些值可以隐式地相互转换。所有内置类型都是如此。我们可以把char转换成int,int转换成double等等。示例:
int main()
{
char mychar = 64;
int myint = 123;
double mydouble = 456.789;
bool myboolean = true;
myint = mychar;
mydouble = myint;
mychar = myboolean;
}
我们也可以隐式地将double转换成int。但是,有些信息会丢失,编译器会警告我们这一点。这叫做**:**
int main()
{
int myint = 123;
double mydouble = 456.789;
myint = mydouble; // the decimal part is lost
}
当更小的整数类型如char或short用于算术运算时,它们被提升/转换为整数。这就是所谓的积分提升 *。*例如,如果我们在一个算术运算中使用两个字符,它们都被转换成一个整数,整个表达式的类型是 int。这种转换只发生在算术表达式中:
int main()
{
char c1 = 10;
char c2 = 20;
auto result = c1 + c2; // result is of type int
}
任何内置类型都可以转换为布尔值。对于这些类型的对象,除 0 之外的任何值都被转换为布尔值true,等于 0 的值被隐式转换为值false。示例:
int main()
{
char mychar = 64;
int myint = 0;
double mydouble = 3.14;
bool myboolean = true;
myboolean = mychar; // true
myboolean = myint; // false
myboolean = mydouble; // true
}
相反,布尔类型可以转换为int。true的值转换为整数值 1,false的值转换为整数值 0。
任何类型的指针都可以转换成void*类型。我们将整数指针转换为空指针的示例:
int main()
{
int x = 123;
int* pint = &x;
void* pvoid = pint;
}
虽然我们可以将任何数据指针转换为空指针,但是我们不能取消对空指针的引用。为了能够访问 void 指针所指向的对象,我们需要先将 void 指针转换为其他类型的指针。为此,我们可以使用下一章中描述的显式转换函数static_cast:
#include <iostream>
int main()
{
int x = 123;
int* pint = &x;
void* pvoid = pint; // convert from int pointer
int* pint2 = static_cast<int*>(pvoid); // cast a void pointer to int // pointer
std::cout << *pint2; // dereference a pointer
}
数组可以隐式转换为指针。当我们给指针分配一个数组名时,指针指向数组中的第一个元素。示例:
#include <iostream>
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
int* p = arr; // pointer to the first array element
std::cout << *p;
}
在这种情况下,我们有一个从类型 int[] 到类型 int* 的隐式转换。
当用作函数参数时,数组被转换为指针。更准确地说,它被转换成指向数组中第一个元素的指针。在这种情况下,数组失去了它的维度,据说它衰减为指针。示例:
#include <iostream>
void myfunction(int arg[])
{
std::cout << arg;
}
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
myfunction(arr);
}
这里, arr 参数被转换成指向数组中第一个元素的指针。因为 arg 现在是一个指针,打印它输出一个类似于 012FF6D8 的指针值。而不是它所指向的值要输出它所指向的值,我们需要取消对指针的引用:
#include <iostream>
void myfunction(int arg[])
{
std::cout << *arg;
}
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
myfunction(arr);
}
话虽如此,重要的是采用以下:首选 std:vector 和 std::array 容器,而不是原始数组和指针。
33.2 显式转换
我们可以显式地将一种类型的值转换成另一种类型。让我们从static_cast函数开始。该函数在隐式可转换类型之间进行转换。该函数的一个特征是:
static_cast<type_to_convert_to>(value_to_convert_from)
如果我们想从一个double转换到int,我们写:
int main()
{
auto myinteger = static_cast<int>(123.456);
}
比起隐式转换,更喜欢这个冗长的函数,因为static_cast是在可转换类型之间转换的惯用方式。该函数执行编译时转换。
下面的显式转换函数应该谨慎使用很少使用。他们是dynamic_cast和reintepret_cast。dynamic_cast函数将基类的指针转换成派生类的指针,反之亦然。示例:
#include <iostream>
class MyBaseClass {
public:
virtual ~MyBaseClass() {}
};
class MyDerivedClass : public MyBaseClass {};
int main()
{
MyBaseClass* base = new MyDerivedClass;
MyDerivedClass* derived = new MyDerivedClass;
// base to derived
if (dynamic_cast<MyDerivedClass*>(base))
{
std::cout << "OK.\n";
}
else
{
std::cout << "Not convertible.\n";
}
// derived to base
if (dynamic_cast<MyBaseClass*>(derived))
{
std::cout << "OK.\n";
}
else
{
std::cout << "Not convertible.\n";
}
delete base;
delete derived;
}
如果转换成功,结果是一个指向基类或派生类的指针,这取决于我们的用例。如果转换不能完成,结果是一个值为nullptr的指针。
要使用这个函数,我们的类必须是多态的,这意味着我们的基类应该至少有一个虚函数。要尝试将一些不相关的类转换为继承链中的一个类,我们可以使用:
#include <iostream>
class MyBaseClass {
public:
virtual ~MyBaseClass() {}
};
class MyDerivedClass : public MyBaseClass {};
class MyUnrelatedClass {};
int main()
{
MyBaseClass* base = new MyDerivedClass;
MyDerivedClass* derived = new MyDerivedClass;
MyUnrelatedClass* unrelated = new MyUnrelatedClass;
// base to derived
if (dynamic_cast<MyUnrelatedClass*>(base))
{
std::cout << "OK.\n";
}
else
{
std::cout << "Not convertible.\n";
}
// derived to base
if (dynamic_cast<MyUnrelatedClass*>(derived))
{
std::cout << "OK.\n";
}
else
{
std::cout << "Not convertible.\n";
}
delete base;
delete derived;
delete unrelated;
}
这将失败,因为dynamic_cast只能在继承链内的相关类之间转换。实际上,我们在现实世界中几乎不需要使用dynamic_cast。
第三种也是最危险的类型是reintrepret_cast .这种类型最好避免,因为它不提供任何形式的保证。考虑到这一点,我们将跳过它的描述,进入下一章。
重要提示:static_cast函数可能是我们大部分时间会用到的唯一类型。*
三十四、异常
如果我们的程序出现错误,我们希望能够以某种方式处理它。一种方法是通过异常。异常是我们试图在 try{}块中执行一些代码的机制,如果出现错误,就会抛出异常。然后,控制被转移到 catch 子句,该子句处理该异常。try/catch 块的结构应该是:
int main()
{
try
{
// your code here
// throw an exception if there is an error
}
catch (type_of_the_exception e)
{
// catch and handle the exception
}
}
一个简单的 try/catch 示例是:
#include <iostream>
int main()
{
try
{
std::cout << "Let's assume some error occurred in our program." << '\n';
std::cout << "We throw an exception of type int, for example." << '\n';
std::cout << "This signals that something went wrong." << '\n';
throw 123; // throw an exception if there is an error
}
catch (int e)
{
// catch and handle the exception
std::cout << "Exception raised!." << '\n';
std::cout << "The exception has a value of " << e << '\n';
}
}
说明:这里我们尝试执行try块中的代码。如果出现错误,我们会抛出一个异常,表示出现了问题。我们例子中的异常是 int 类型的,但是它可以是任何类型的。当抛出异常时,控制被转移到一个catch子句,该子句处理异常。在我们的例子中,它处理类型int的异常。
我们可以抛出不同类型的异常,std::string例如:
#include <iostream>
#include <string>
int main()
{
try
{
std::cout << "Let's assume some error occured in our program." << '\n';
std::cout << "We throw an exception of type string
, for example." << '\n';
std::cout << "This signals that something went wrong." << '\n';
throw std::string{ "Some string error" }; // throw an exception // if there is an error
}
catch (const std::string& e)
{
// catch and handle the exception
std::cout << "String exception raised!." << '\n';
std::cout << "The exception has a value of: " << e << '\n';
}
}
我们可以有/引发多个异常。它们可以是不同的类型。在这种情况下,我们有一个 try 和多个 catch 块。每个 catch 块处理不同的异常。
#include <iostream>
#include <string>
int main()
{
try
{
throw 123;
// the following will not execute as
// the control has been transferred to a catch clause
throw std::string{ "Some string error" };
}
catch (int e)
{
std::cout << "Integer exception raised! The value is " << e << '\n';
}
catch (const std::string& e)
{
// catch and handle the exception
std::cout << "String exception raised!." << '\n';
std::cout << "The exception has a value of: " << e << '\n';
}
}
这里我们在 try 块中抛出了多个异常。第一个是int类型,第二个是std::string类型。当第一个异常被抛出时,程序的控制权被转移到一个 catch 子句。这意味着 try 块中的剩余代码将不会被执行。
更现实的情况是:
#include <iostream>
#include <string>
int main()
{
try
{
bool someflag = true;
bool someotherflag = true;
std::cout << "We can have multiple throw exceptions." << '\n';
if (someflag)
{
std::cout << "Throwing an int exception." << '\n';
throw 123;
}
if(someotherflag)
{
std::cout << "Throwing a string exception." << '\n';
throw std::string{ "Some string error" };
}
}
catch (int e)
{
// catch and handle the exception
std::cout << "Integer exception raised!." << '\n';
std::cout << "The exception has a value of: " << e << '\n';
}
catch (const std::string& e)
{
// catch and handle the exception
std::cout << "String exception raised!." << '\n';
std::cout << "The exception has a value of: " << e << '\n';
}
}
这里我们在 try 块中抛出了多个异常。出于说明的目的,它们依赖于一些条件。当遇到第一个异常时,控制被转移到适当的 catch 子句。
三十五、智能指针
智能指针是拥有它们所指向的对象的指针,并且一旦指针超出范围,就自动销毁它们所指向的对象并释放内存。这样,我们不必像使用 new 和 delete 操作符那样手动删除对象。
智能指针在<memory>头中声明。我们将讨论以下智能指针——唯一的和共享的。
35.1 唯一指针
名为std::unique_ptr的唯一指针是一个拥有它所指向的对象的指针。指针不能被复制。一旦对象超出作用域,唯一指针就删除该对象并为其释放内存。为了声明一个简单 int 对象的唯一指针,我们写:
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> p(new int{ 123 });
std::cout << *p;
}
这个例子创建了一个指向类型为int的对象的指针,并将值123赋给这个对象。使用*p符号,唯一指针可以像普通指针一样被解引用。一旦 p 超出范围,对象就会被删除,在本例中,p 位于右括号}处。不需要显式使用删除运算符。
一个更好的初始化唯一指针的方法是通过一个std::make_unique<some_type>(some_value)函数,其中我们在尖括号中指定对象的类型,在括号中指定对象指针指向的值:
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> p = std::make_unique<int>(123);
std::cout << *p;
}
在 C++14 标准中引入了std::make_unique函数。确保使用 -std=c++14 标志进行编译,以便能够使用该函数。
我们可以创建一个指向一个类的对象的唯一指针,然后使用它的->操作符来访问对象成员:
#include <iostream>
#include <memory>
class MyClass
{
public:
void printmessage()
{
std::cout << "Hello from a class.";
}
};
int main()
{
std::unique_ptr<MyClass> p = std::make_unique<MyClass>();
p->printmessage();
}
一旦 p 超出范围,对象就会被销毁。所以,比起原始指针和它们的新删除机制,更喜欢一个唯一的指针。一旦 p 超出范围,类的指向对象就会被销毁。
我们可以使用一个唯一的指针来利用多态类:
#include <iostream>
#include <memory>
class MyBaseClass
{
public:
virtual void printmessage()
{
std::cout << "Hello from a base class.";
}
};
class MyderivedClass: public MyBaseClass
{
public:
void printmessage()
{
std::cout << "Hello from a derived class.";
}
};
int main()
{
std::unique_ptr<MyBaseClass> p = std::make_unique<MyderivedClass>();
p->printmessage();
}
整洁哈?不需要显式删除分配的内存,智能指针为我们做了。因此有了智能部分。
35.2 共享指针
我们可以让多个指针指向一个对象。我们可以说它们都拥有我们指向的对象,也就是我们的对象拥有共享所有权。只有当最后一个指针被销毁时,我们指向的对象才会被删除。这就是共享指针的用途。多个指针指向一个对象,当所有指针都超出作用域时,该对象就会被销毁。
共享指针定义为std::shared_ptr<some_type>。可以使用std::make_shared<some_type>(some_value)函数进行初始化。共享指针可以复制。要让三个共享指针指向同一个对象,我们可以写:
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> p1 = std::make_shared<int>(123);
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3 = p1;
}
当所有指针都超出范围时,所指向的对象被销毁,并且它的内存被释放。
唯一指针和共享指针之间的主要区别是:
-
对于唯一指针,我们有一个指针指向并拥有一个对象,而对于共享指针,我们有多个指针指向并拥有一个对象。
-
唯一指针不能被复制,而共享指针可以。
如果您想知道使用哪一个,假设 90%的时间,您将使用唯一指针。共享指针可以用来表示数据结构,比如图形。
智能指针本身就是类模板,这意味着它们有成员函数。我们将简单地提到它们也可以接受自定义删除器,这是一个当它们超出范围时被执行的代码。
注意,对于智能指针,我们不需要指定<some_type*>,我们只需要指定<some_type>.
重要!
比起原始指针,更喜欢智能指针。有了智能指针,我们不必担心是否正确地匹配了对new的调用和对delete的调用,因为我们不需要它们。我们让智能指针做所有繁重的工作。
三十六、练习
36.1 静态转换
编写一个使用 static_cast 函数在基本类型之间转换的程序。
#include <iostream>
int main()
{
int x = 123;
double d = 456.789;
bool b = true;
double doubleresult = static_cast<double>(x);
std::cout << "Int to double: " << doubleresult << '\n';
int intresult = static_cast<int>(d); // double to int
std::cout << "Double to int: " << intresult << '\n';
bool boolresult = static_cast<bool>(x); // int to bool
std::cout << "Int to bool: " << boolresult << '\n';
}
36.2 一个简单的唯一指针:
编写一个程序,定义一个唯一的整数值指针。使用 std::make_unique 函数创建一个指针。
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> p = std::make_unique<int>(123);
std::cout << "The value of a pointed-to object is: " << *p << '\n';
}
36.3 指向类对象的唯一指针
编写一个程序,用两个数据成员、一个用户定义的构造器和一个成员函数定义一个类。创建一个指向类对象的唯一指针。使用智能指针访问成员函数。
#include <iostream>
#include <memory>
class MyClass
{
private:
int x;
double d;
public:
MyClass(int xx, double dd)
: x{ xx }, d{ dd }
{}
void printdata()
{
std::cout << "Data members values are: " << x << " and: " << d;
}
};
int main()
{
std::unique_ptr<MyClass> p = std::make_unique<MyClass>(123, 456.789);
p->printdata();
}
36.4 共享指针练习
编写一个程序,定义三个共享指针,指向同一个类型为 int 的对象。通过 std::make_shared 函数创建第一个指针。通过复制第一个指针来创建其余的指针。通过所有指针访问所指向的对象。
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> p1 = std::make_shared<int>(123);
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3 = p1;
std::cout << "Value accessed through a first pointer: " << *p1 << '\n';
std::cout << "Value accessed through a second pointer: " << *p2 << '\n';
std::cout << "Value accessed through a third pointer: " << *p3 << '\n';
}
简单多态性
写一个用纯虚拟成员函数定义基类的程序。创建一个派生类,该派生类重写基类中的虚函数。通过指向基类的唯一指针创建派生类的多态对象。通过唯一指针调用重写的成员函数。
#include <iostream>
#include <memory>
class BaseClass
{
public:
virtual void dowork() = 0;
virtual ~BaseClass() {}
};
class DerivedClass : public BaseClass
{
public:
void dowork() override
{
std::cout << "Do work from a DerivedClass." << '\n';
}
};
int main()
{
std::unique_ptr<BaseClass> p = std::make_unique<DerivedClass>();
p->dowork();
} // p1 goes out of scope here
这里的覆盖描述符明确声明了派生类中的 dowork() 函数覆盖了基类中的虚函数。
这里,我们使用唯一的指针来创建并自动销毁对象,并在指针超出 main() 函数的范围时释放内存。
36.6 多态性 II
写一个用纯虚拟成员函数定义基类的程序。从基类派生两个类,并重写虚函数行为。创建两个基类类型的唯一指针,指向这些派生类的对象。使用指针来调用适当的多态行为。
#include <iostream>
#include <memory>
class BaseClass
{
public:
virtual void dowork() = 0;
virtual ~BaseClass() {}
};
class DerivedClass : public BaseClass
{
public:
void dowork() override
{
std::cout << "Do work from a DerivedClass." << '\n';
}
};
class SecondDerivedClass : public BaseClass
{
public:
void dowork() override
{
std::cout << "Do work from a SecondDerivedClass." << '\n';
}
};
int main()
{
std::unique_ptr<BaseClass> p = std::make_unique<DerivedClass>();
p->dowork();
std::unique_ptr<BaseClass> p2 = std::make_unique<SecondDerivedClass>();
p2->dowork();
} // p1 and p2 go out of scope here
36.7 异常处理
编写一个抛出并捕获整数异常的程序。处理异常并打印其值:
#include <iostream>
int main()
{
try
{
std::cout << "Throwing an integer exception with value of 123..." << '\n';
int x = 123;
throw x;
}
catch (int ex)
{
std::cout << "An integer exception of value: " << ex << " caught and handled." << '\n';
}
}
36.8 多重例外
编写一个可以在同一个 try 块中抛出 integer 和 double 异常的程序。为这两个异常实现异常处理块。
#include <iostream>
int main()
{
try
{
std::cout << "Throwing an int exception..." << '\n';
throw 123;
std::cout << "Throwing a double exception..." << '\n';
throw 456.789;
}
catch (int ex)
{
std::cout << "Integer exception: " << ex << " caught and handled." << '\n';
}
catch (double ex)
{
std::cout << "Double exception: " << ex << " caught and handled." << '\n';
}
}