内存泄漏
内存泄漏(Memory Leak)是指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
A memory leak is a particular type of unintentional memory consumption by a computer program where the program fails to release memory when no longer needed.
无论是C还是C++程序,运行时候的变量主要有三种分配方式:堆分配、栈分配、全局和静态内存分配。内存泄漏主要是发生在堆内存分配方式中,即“配置了内存后,所有指向该内存的指针都遗失了”,若缺乏语言这样的垃圾回收机制,这样的内存片就无法归还系统。
对于堆栈中的局部变量,管理内存是自动执行的。
在任何时候,当您觉得某个已经动态分配内存的变量不再需要使用时,您可以使用 delete 运算符释放它所占用的内存。
int *p = new int; // 为变量请求内存
*p = 10; // 在分配的地址存储值
cout << *p << endl;
delete p; // 释放内存
rand() 函数
能够生成随机数,在很多情况下都很有用,包括创建游戏,统计建模程序和类似的最终产品。
在C++ 标准库中,可以访问被称为rand()的伪随机数生成器函数。在使用时,我们需要包含头文件。
下面是一个示例:
#include <iostream>
#include <cstdlib>
using namespace std;
int main() {
cout << rand();
}
这将输出一个随机数。
模(%) 运算符生成随机数
在 C++ 中,可以使用模(%) 运算符来生成特定范围内的随机数。
int a = rand() % 10; //产生0~9的随机数,注意10会被整除
int a = rand() % 51 + 13; //产生13~63的随机数
下面是一个例子,生成1到5范围内的整数
int main () {
for (int x = 1; x <= 10; x++) {
cout << 1 + (rand() % 5) << endl;
}
}
/* 输出:
2
3
5
1
5
5
4
4
3
5
*/
但是,rand() 函数只会返回一个伪随机数。这意味着每次运行代码时,都会生成相同的编号。
实际上,rand() 函数产生的随机数是伪随机数,是根据一个数值按照某个公式推算出来的,这个数值我们称之为“种子”。种子和随机数之间的关系是一种正态分布,如下图所示:

C语言随机数和种子之间呈正态分布
种子在每次启动计算机时是随机的,但是一旦计算机启动以后它就不再变化了;也就是说,每次启动计算机以后,种子就是定值了,所以根据公式推算出来的结果(也就是生成的随机数)就是固定的。
srand()函数重新播种
我们可以通过 srand()
函数来重新“播种”,这样种子就会发生改变。srand()
的用法为:
void srand (unsigned int seed);
它需要一个 unsigned int
类型的参数。在实际开发中,我们可以用时间作为参数,只要每次播种的时间不同,那么生成的种子就不同,最终的随机数也就不同。
使用 <ctime>
头文件中的 ctime()
函数即可得到当前的时间(精确到秒),就像下面这样:
srand((unsigned)time(NULL));
或者srand(time(0));
time(0) 将返回当前秒数,提示srand() 函数每次程序运行时为rand() 函数设置一个不同的种子。
生成随机数之前先进行播种:
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
int main() {
int a;
srand((unsigned)ctime(NULL));
a = rand();
cout << a << endl;
return 0;
}
多次运行程序,会发现每次生成的随机数都不一样了。但是,这些随机数会有逐渐增大或者逐渐减小的趋势,这是因为我们以时间为种子,时间是逐渐增大的,结合上面的正态分布图,很容易推断出随机数也会逐渐增大或者减小。
函数重载
1、在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型)必须不同。
例如,您可能需要一个Test() 函数来打印其参数的值。
void Test(int a) {
cout << a;
}
这只对整型参数有效。重载它将使其可用于其他类型,如浮点型。
void Test(float a) {
cout << a;
}
现在,相同的Test() 函数名称将适用于整数和浮点数。
2、当重载函数时,函数的定义必须根据参数列表中的参数的个数、或类型而不同。
下面是一个例子:
void Test(int x) {
cout << "整数为: " << x << endl;
}
void Test(float x) {
cout << "浮点数为: " << x << endl;
}
int main() {
int a = 30;
float b = 68.652;
Test(a);
Test(b);
}
/* 输出:
整数为: 30
浮点数为: 68.652
*/
如你所见,函数调用是基于提供的参数。一个整数参数将调用带有整数参数的函数实现,一个浮点数参数将调用执行一个浮点数参数。
3、您不能仅通过返回类型的不同来重载函数。
下面的声明将导致错误。
int Test(int a) {}
float Test(int b) {}
string Test(int c) {}
虽然每个函数都使用相同的名称,但唯一的区别是返回类型,这是不允许的。
递归
在C++中,递归函数是一个自我调用的函数。
为了避免递归无限地运行,您必须包含终止条件。
现在,让我们来定义我们的函数:
int factorial(int n) {
if (n==1) {
return 1;
}
else {
return n * factorial(n-1);
}
}
if 语句定义退出条件。在这种情况下,当n等于1时,返回1(1的阶乘是1)。
我们将递归函数调用放在else语句中,返回n乘以n-1的阶乘。
例如,如果使用参数3调用阶乘函数,它将执行如下: 返回3 * factorial(2),即3 * 2 * factorial(1),即3 * 2 * 1。
提示:阶乘(factorial)函数自我调用,然后一直继续,直到参数等于1。
现在,我们可以称之为阶乘函数了。
int factorial(int n) {
if (n==1) {
return 1;
}
else {
return n * factorial(n-1);
}
}
int main() {
cout << factorial(3);
}
//输出 6
退出条件的另一个名称是基本情况。
提示:一个退出条件(基本情况)是真正的递归所必需的。没有它,递归将永远持续运行。
将数组传递给函数
在 C++ 中,一个数组也可以作为参数传递给一个函数。
声明函数时,参数应该用方括号[] 来定义。
声明函数之后,我们可以在main() 中使用我们的函数,并在数组中调用它。
void Array(int arr[], int size) {
for(int x=0; x<size; x++) {
cout << arr[x] << endl;
}
}
int main() {
int myArr[3]= {17, 30, 28};
Array(myArr, 3);
}
在代码中,Array函数将数组作为参数(int arr[]),并使用for循环遍历数组。
我们在main() 中调用函数,这是我们将myArr数组传递给打印元素的函数的地方。
提示:请记住,要将数组作为参数传递给函数,请指定不带方括号的数组名称。
例、声明一个函数sum(),它将一个数组及其大小作为参数,并计算数组元素的总和打印出来
void sum(int arr[], int size)
{
int result = 0;
for(int x = 0; x < size; x++)
{
result += arr[x];
}
cout << result << endl;
}
函数传递参数的方式
当调用函数时,有两种向函数传递参数的方式:
传值调用:该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数对实际参数没有影响。
引用调用:该方法把参数的引用复制给形式参数。在函数内,该引用用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
提示:默认情况下,C++ 使用传值调用来传递参数。
传值调用
向函数传递参数的传值调用方法,把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
默认情况下,C++ 使用传值调用方法来传递参数。一般来说,这意味着函数内的代码不会改变用于调用函数的实际参数。
下面是一个例子:
void swap(int x) {
x = 50;
}
int main() {
int var = 10;
swap(var);
cout << var;
}
// 输出 10
引用调用
向函数传递参数的引用调用方法,把引用的地址复制给形式参数。在函数内,该引用用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
按引用传递值,参数引用被传递给函数,就像传递其他值给函数一样。
下面是一个例子:
void swap(int *x) {
*x = 50;
}
int main() {
int var = 10;
swap(&var);
cout << var;
}
// 输出 50
如你所见,我们使用操作符&的地址将变量直接传递给函数。
函数声明表示该函数将一个指针作为其参数(使用 * 运算符定义)。
结果,函数实际上已经改变了参数的值,通过指针访问它。
传值调用与引用调用的比较
传值调用:修改函数内的形式参数对实际参数没有影响。
引用调用:修改形式参数会影响实际参数。
一般来说,传值调用更快,更有效。当函数需要修改参数时,或者当需要传递一个数据类型时,如果通过引用调用,这会占用大量的内存,而且复制的代价很高。
对象
OOP
面向对象编程(Object Oriented Programming)是一种编程风格,旨在让思维编程更接近思考现实世界。
在编程世界里,对象是独立单元,每一个都有自己的身份,像真实世界那样,像苹果,马克杯等等,每一个都有独一无二的 identity (身份)。
有两个一模一样的马克杯,但是他们仍然是分开的两个独立个体。
属性
一个对象可能包含其他对象,但它们仍然是不同的对象。
对象用属性来描述它们的特征。例如,一辆车可以是红色或蓝色,一个杯子可以是满的或空的,等等。
一个属性描述了一个对象的当前状态。
对象可以有多个属性(马克杯可以是空的,红色的和大的)。
对象的状态与其类型无关;比如:一个杯可能充满水,另一个杯可能是空的。
对象类型的三个维度
在现实世界中,每个对象都以自己的方式行事。汽车是移动,电话是铃响,等等。这同样适用于对象。
对象的类型有三个维度表示,identity(标识),attributes(属性),behavior(行为)
对象的属性
在编程中,一个对象是独立的,有自己的身份(标识)。它与其他对象是分开的。
每个对象都有自己的属性,描述了它的当前状态。每个对象都展示自己的行为,这表明他们可以做什么。

在编程中,对象并不总是代表物理项目。
例如,一个编程对象可以代表日期,时间,银行账户。银行账户不是有形的;你看不见它或者触摸它,但它仍然是一个明确的对象 - 它有自己的身份,属性和行为。
什么是类
什么是类
对象是使用类来创建的,而这些类实际上是 OOP(面向对象编程) 的焦点。
这个类描述了这个对象是什么,但是和对象本身是分开的。
换句话说,一个类可以被描述为一个对象的蓝图,描述或定义。
您可以使用相同的类作为创建多个不同对象的蓝图。
例如,为了创建新的建筑,建筑师创建了一个蓝图,作为实际建筑结构的基础。同样的蓝图可以用来创建多个建筑物。
编程以相同的方式工作。我们首先定义一个类,它成为创建对象的蓝图。
每个类都有一个名称,并描述了属性和行为。
在编程中,type 类型用于引用类名:我们正在创建一个特定类型的对象。
属性也被称为属性或数据。
方法
方法是类行为的另一个术语。一个方法基本上是一个属于一个类的函数。
方法类似于函数 - 它们是被调用的代码块,也可以执行操作并返回值。
一个类的例子
例如,如果我们正在创建一个银行项目,我们可以给我们的类以下特征:
名称:BankAccount
属性:accountNumber,余额: dateOpened
行为:open(), close(), deposit()
该类指定每个对象应具有已定义的属性和行为。但是,它没有指定实际的数据是什么;它只提供一个定义。
一旦我们编写了这个类,我们就可以继续创建基于这个类的对象。
每个对象被称为一个类的实例。创建对象的过程称为实例化。
每个对象都有自己的身份(标识),数据(属性)和行为。
类的例子
声明一个类1
用关键字 class 定义类。按照带有类名和类体的关键字,用一组大括号(花括号)括起来。
以下代码声明了一个名为 BankAccount 的类:
class BankAccount {
};
一个类的定义必须跟一个分号。
声明一个类2
在大括号内定义类的所有属性和行为(或成员)。
您还可以为类的成员定义访问说明符。
已经使用 public 关键字定义的成员可以从类外部访问,只要它在类对象范围内的任何位置。
你也可以指定一个类的成员为 private 或 protected 的。
创建一个类
让我们用一个公开的方法创建一个类,并打印出 "Hi,school"。
class BankAccount {
public:
void sayHi() {
cout << "Hi,school" << endl;
}
};
下一步是实例化我们的 BankAccount 类的一个对象,就像我们定义一个类型的变量一样,区别在于我们的对象的类型是 BankAccount。
int main()
{
BankAccount test;
test.sayHi();
}
我们的名为 test 的对象已经拥有了所有类的成员。
注意用于访问和调用对象的方法的点分隔符 . (英文点符号)
我们必须在使用它之前声明一个类,就像我们对函数做的一样。
抽象
抽象1
数据抽象是向外界提供唯一重要信息的概念。
这是一个表示基本特征而不包括实现细节的过程。
一个好的现实世界的例子是一本书:当你听到书,你不知道确切的细节,即页数,颜色,大小,但你明白一本书的想法 - 抽象这本书。
抽象的概念是我们关注基本本质,而不是一个特定例子的具体特征。
抽象2
抽象意味着我们可以有一个完全与任何具体实例分离的想法或概念。
它是面向对象编程的基本组成部分之一。
例如,当你使用 cout 时,你实际上使用了类 ostream 的 cout 对象。使流数据标准输出。
cout << "Hello Loen!" << endl;
在这个例子中,不需要知道 cout 如何在用户的屏幕上显示文本。
你需要知道的是能够使用它,它是公共接口。
抽象3
抽象允许我们写一个单一的银行账户类,然后根据类创建不同的对象。

封装
封装1
封装这个词的部分含义是 “围绕” 一个实体的想法,不仅仅是把内在的东西放在一起,而且还要保护它。
在面向对象方面,封装不仅仅是将一个类中的属性和行为组合在一起,这也意味着限制进入该类的内部工作。
这里的关键原则是一个对象只显示其他应用程序组件需要有效运行应用程序的内容。其他一切都被保留在视野之外(隐藏)。
封装2
例如,如果我们使用 BankAccount 类,我们不希望程序的其他部分进入并更改任何对象的 balance ,而无需通过 deposit() 或 withdraw() 行为。
我们应该隐藏这个属性,控制对它的访问,所以它只能被对象本身访问。这样,balance 不能直接从物体外面改变,只能用其方法进行访问。
这也被称为“黑匣子”,是指关闭对象的内部工作区域,除了我们想要公开的部分。
这使我们可以在不改变整个程序的情况下改变方法的属性和实现。例如,我们可以稍后再回来,更改 balance 属性的数据类型。
总之封装的好处是:
- 控制数据访问或修改的方式。
- 代码更加灵活,易于根据新的要求进行更改。
- 更改代码的一部分而不影响其他代码部分。
封装示例
访问说明符1
访问说明符用于为特定成员设置访问级别。
访问说明符的三个级别是 public, protected, 和 private.
一个 public 成员可以从类以外的任何地方访问,也可以在类对象的范围内的任何地方访问。
例如:
#include <iostream>
#include <string>
using namespace std;
class myClass {
public:
string name;
};
int main() {
myClass myObj;
myObj.name = "Eeedong Loen";
cout << myObj.name;
return 0;
}
//输出 "Eeedong Loen"
name 属性是 public (公共的);它可以从代码之外访问和修改。
访问修饰符只需要声明一次; 多个成员可以遵循单个访问修饰符。注意 public 关键字后面的冒号(??
Private
一个 private 成员不能从类外访问(对外不可见);只能从类内部进行访问。
#include <iostream>
#include <string>
using namespace std;
class myClass {
public:
void setName(string x) {
name = x;
}
private:
string name;
};
int main() {
myClass myObj;
myObj.setName("Loen");
return 0;
}
name 属性是 private 私有的,不能从外部访问。
setName() 方法是 public 并在方法中使用到了 name 属性
如果未定义访问说明符,则默认情况下,类的所有成员都设置为 private。
访问说明符2
我们可以添加另一个公共方法来获取属性的值。
class myClass {
public:
void setName(string x) {
name = x;
}
string getName() {
return name;
}
private:
string name;
};
getName() 方法返回 私有属性 name 的值。
访问说明符3
把访问说明符放在一起:
#include <iostream>
#include <string>
using namespace std;
class myClass {
public:
void setName(string x) {
name = x;
}
string getName() {
return name;
}
private:
string name;
};
int main() {
myClass myObj;
myObj.setName("Loen");
cout << myObj.getName();
return 0;
}
//输出 "Loen"
我们使用封装来隐藏来自外部代码的名称属性。然后我们使用公共方法提供访问权限。我们的类数据只能通过这些方法读取和修改。
这样允许更改方法和属性的实现,而不会影响外部代码。
构造函数
构造函数1
类构造函数是类的特殊成员函数。无论何时在该类中创建新对象,都会执行它们。
构造函数的名称与该类的名称完全相同。它没有返回类型,甚至 void 类型。
例如:
class myClass {
public:
myClass() {
cout <<"Loen";
}
void setName(string x) {
name = x;
}
string getName() {
return name;
}
private:
string name;
};
int main() {
myClass myObj;
return 0;
}
//输出 "Loen"
在创建一个 myClass 类型的对象时,构造函数会自动调用。
构造函数2
构造函数对于设置某些成员变量的初始值非常有用。
默认的构造函数没有参数。但是,在需要时,可以将参数添加到构造函数中。
这可以在创建对象时为其分配一个初始值,如下例所示:
class myClass {
public:
myClass(string nm) {
setName(nm);
}
void setName(string x) {
name = x;
}
string getName() {
return name;
}
private:
string name;
};
我们定义了一个构造函数,该构造函数接受一个参数,并使用 setName() 方法将其分配给 name 属性。
构造函数3
创建对象时,您现在需要传递构造函数的参数,就像调用函数时一样:
class myClass {
public:
myClass(string nm) {
setName(nm);
}
void setName(string x) {
name = x;
}
string getName() {
return name;
}
private:
string name;
};
int main() {
myClass ob1("eeedong");
myClass ob2("Amy");
cout << ob1.getName();
}
//输出 "eeedong"
我们定义了两个对象,并使用构造函数传递每个对象的 name 属性的初始值。
可能有多个构造函数他们有不同数量的参数。
类分布于文件
创建一个新的类
在不同的文件中定义新的类通常是一个好习惯。这使得维护和阅读代码更容易。
为此,请在 CodeBlocks 中使用以下步骤:
点击 File->New->Class...
给你的新类一个名字,取消选中 “Has destructor”(具有析构函数),勾选 "Header and implementation file shall be in same folder"(头文件和实现文件应该在同一个文件夹中),然后点击 "Create"(创建) 按钮。

请注意,两个新文件已添加到您的项目中:

新类模板。
- MyClass.h 是头文件。
- MyClass.cpp 是源文件。
源文件和头文件1
头文件(.h)包含函数声明(原型)和变量声明。
它目前包括一个我们新的 MyClass 类的模板,带有一个默认的构造函数。
MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_H
class MyClass
{
public:
MyClass();
protected:
private:
};
#endif // MYCLASS_H
类的实现及其方法在源文件(.cpp)。
目前它只包含一个空的构造函数。
MyClass.cpp
#include "MyClass.h"
MyClass::MyClass()
{
//ctor
}
头文件中的 #ifndef 和 #define 语句将在即将到来的课程中讨论。
范围解析运算符
源文件(.cpp)中的双冒号称为作用域解析运算符,用于构造函数定义:
#include "MyClass.h"
MyClass::MyClass()
{
//ctor
}
范围解析运算符用于定义已经声明的特定类的成员函数。 请记住,我们在头文件中定义了构造函数原型。
所以,MyClass::MyClass() 引用 MyClass() 成员函数,或者在这种情况下 MyClass 类的构造函数。
头文件与源文件2
要在 main 中使用我们的类,我们需要包含我们的头文件。
例如,要在 main 中使用我们新创建的 MyClass:
#include <iostream>
#include "MyClass.h"
using namespace std;
int main() {
MyClass obj;
}
头文件声明了一个类将做什么,而 cpp 源文件定义了它将如何执行这些功能。
析构函数
析构函数1
析构函数的名字将与该类完全相同,只是有前缀(~)。析构函数不能返回值或取任何参数。
class MyClass {
public:
~MyClass() {
// some code
}
};
析构函数在退出程序之前可以非常有用地释放资源。这可以包括关闭文件,释放内存等等。
析构函数2
例如,我们在头文件 MyClass.h 中为我们的 MyClass 类声明一个析构函数:
class MyClass
{
public:
MyClass();
~MyClass();
};
析构函数3
在头文件中声明析构函数后,我们可以在源文件 MyClass.cpp 中编写实现:
#include "MyClass.h"
#include <iostream>
using namespace std;
MyClass::MyClass()
{
cout<<"Constructor"<<endl;
}
MyClass::~MyClass()
{
cout<<"Destructor"<<endl;
}
请注意,我们包含 ,以便我们可以使用 cout。
析构函数4
由于析构函数不能带参数,所以也不能重载。每个类将只有一个析构函数。
定义析构函数不是强制性的。如果你不需要一个,你可以不定义。
析构函数5
让我们回到主函数
#include <iostream>
#include "MyClass.h"
using namespace std;
int main() {
MyClass obj;
return 0;
}
我们包含了类的头文件,然后创建了这种类型的对象。
这将返回以下输出:
Constructor
Destructor
程序运行时,首先创建对象并调用构造函数。当程序执行完成时,对象被删除,析构函数被调用。
请记住,我们在构造函数中打印 “Constructor”,在析构函数中打印 “Destructor”。
选择操作
#ifndef & #define
我们为我们的类创建了单独的头文件和源文件,生成的头文件内容如下。
#ifndef MYCLASS_H
#define MYCLASS_H
class MyClass
{
public:
MyClass();
protected:
private:
};
#endif // MYCLASS_H
ifndef 代表“如果没有定义”。
endif 结束条件。
这可以防止头文件在一个文件中被多次包含。
成员函数
让我们在我们的类中创建一个名为 myPrint() 的示例函数。
MyClass.h
class MyClass
{
public:
MyClass();
void myPrint();
};
MyClass.cpp
#include "MyClass.h"
#include <iostream>
using namespace std;
MyClass::MyClass() {
}
void MyClass::myPrint() {
cout <<"Hello W3Cschool"<<endl;
}
由于 myPrint() 是一个常规的成员函数,因此有必要在声明和定义中指定它的返回类型。
点操作
接下来,我们将创建一个 MyClass 类的对象,并使用点(.)运算符调用它的 myPrint() 函数:
#include "MyClass.h"
int main() {
MyClass obj;
obj.myPrint();
}
// 输出 "Hello W3Cschool"
指针
我们也可以使用指针来访问对象的成员。
以下指针指向obj对象:
MyClass obj;
MyClass *ptr = &obj;
指针的类型是 MyClass,因为它指向该类型的对象。
选择操作符
箭头成员选择操作符( ->)用于通过指针访问对象的成员。
MyClass obj;
MyClass *ptr = &obj;
ptr->myPrint();
使用对象时,使用点成员选择操作符(.)。
使用指向对象的指针时,请使用箭头成员选择操作符( ->)。
常量对象
常量
常量是一个固定值的表达式。程序运行时不能更改。
使用 const 关键字来定义一个常量变量。
const int x = 42;
所有常量变量在创建时都必须初始化。
常量对象1
与内置数据类型一样,我们可以使用 const 关键字使类对象保持不变。
const MyClass obj;
所有 const 变量在创建时都必须被初始化。在类的情况下,这个初始化是通过构造函数完成的。
如果一个类没有使用参数化构造函数进行初始化,则必须提供一个公共的默认构造函数 - 如果没有提供公共的默认构造函数,则会发生编译错误。
一旦 const 类对象已经通过构造函数被初始化,就不能修改对象的成员变量。这包括直接更改公共成员变量和调用成员变量值的成员函数。
当你用 const 来声明一个对象的时候,你不能在对象的生命周期中改变它的数据成员。
常量对象2
只有非 const 对象可以调用非 const 函数。
常量对象不能调用常规函数。因此,对于一个常量的对象来说,你需要一个常量函数。
要将函数指定为 const 成员,const 关键字必须在函数原型之后,在其参数的右括号之外。
对于在类定义之外定义的 const 成员函数,必须在函数原型和定义上使用 const 关键字。
例如:
MyClass.h
class MyClass
{
public:
void myPrint() const;
};
MyClass.cpp
#include "MyClass.h"
#include <iostream>
using namespace std;
void MyClass::myPrint() const {
cout <<"Hello Loen"<<endl;
}
现在 myPrint() 函数是一个常量成员函数。因此,它可以被我们常量对象所调用:
int main() {
const MyClass obj;
obj.myPrint();
}
// 输出 "Hello Loen"
常量对象3
尝试从常量对象调用常规函数会导致错误。
另外,当任何 const 成员函数试图改变成员变量或调用 非const 成员函数时,会产生编译器错误。
定义常量对象和函数可确保相应的数据成员不会被意外修改。
成员初始化
成员初始化程序
回想一下,常量是不能改变的变量,所有 const 变量必须在创建时被初始化。
C++ 提供了一个方便的语法来初始化类成员,称为成员初始化列表(也称为构造函数初始值设定)。
成员初始化1
例如:
class MyClass {
public:
MyClass(int a, int b) {
regVar = a;
constVar = b;
}
private:
int regVar;
const int constVar;
};
这个类有两个成员变量 regVar 和 constVar。有一个构造函数,构造函数有两个参数,用于初始化成员变量。
运行这个代码会返回一个错误,因为它的一个成员变量是一个常量,在声明之后不能赋值。
在像这样的情况下,可以使用成员初始化列表为成员变量赋值。
class MyClass {
public:
MyClass(int a, int b)
: regVar(a), constVar(b)
{
}
private:
int regVar;
const int constVar;
};
请注意,在语法中,初始化列表遵循构造函数参数。该列表以冒号(:)开始,然后列出要初始化的每个变量以及该变量的值,并用逗号分隔它们。
使用语法 变量(值) 来分配值。
初始化列表消除了在构造函数体中放置显式赋值的需要。此外,初始化列表不以分号结束。
成员初始化程序
我们使用单独的头文件和源文件来编写前面的示例。
MyClass.h
class MyClass {
public:
MyClass(int a, int b);
private:
int regVar;
const int constVar;
};
MyClass.cpp
MyClass::MyClass(int a, int b)
: regVar(a), constVar(b)
{
cout << regVar << endl;
cout << constVar << endl;
}
我们在构造函数中添加了 cout 语句来显示成员变量的值。
下一步是在 main 中创建我们的类的对象,并使用构造函数来赋值。
#include "MyClass.h"
int main() {
MyClass obj(32, 56);
}
/*输出
32
56
*/
构造函数用于创建对象,通过成员初始化列表将两个参数分配给成员变量。
成员初始化2
成员初始化列表可以用于常规变量,常量变量。
常量变量必须使用成员初始化列表进行初始化。
即使在成员变量不是常量的情况下,使用成员初始值设定语法也是很有意义的。
组合,第一部分
组合1
在现实世界中,复杂的对象通常是使用更小,更简单的对象来组合的。例如,使用金属框架,发动机,轮胎和大量其他部件来组装汽车。
这个过程被称为组合。
在C++中,对象组合涉及使用类作为其他类中的成员变量。这个示例程序演示了组合。它包含 Person 和 Birthday 类,每个 Person 都有一个生日对象作为其成员。
Birthday:
class Birthday {
public:
Birthday(int m, int d, int y)
: month(m), day(d), year(y)
{
}
private:
int month;
int day;
int year;
};
我们的生日班有三个成员变量。它还有一个使用成员初始化列表初始化成员的构造函数。
为了简单起见,该类在单个文件中被声明。或者,你也可以使用头文件和源文件。
组合2
在我们的 Birthday 类中添加一个 printDate() 函数:
class Birthday {
public:
Birthday(int m, int d, int y)
: month(m), day(d), year(y)
{
}
void printDate()
{
cout<<month<<"/"<<day
<<"/"<<year<<endl;
}
private:
int month;
int day;
int year;
};
组合3
接下来,我们可以创建 Person 类,其中包括 Birthday 类。
#include <string>
#include "Birthday.h"
class Person {
public:
Person(string n, Birthday b)
: name(n),
bd(b)
{
}
private:
string name;
Birthday bd;
};
Person 类有一个 name 和一个 Birthday 成员,并有一个构造函数来初始化它们。
注意: 确保包含相应的头文件。
组合,第二部分
组合1
现在,我们的 Person 类有一个 Birthday 类型的成员:
class Person {
public:
Person(string n, Birthday b)
: name(n),
bd(b)
{
}
private:
string name;
Birthday bd;
};
组合用于共享一个有关系的对象,如 “一个人有一个生日”。
组合2
在我们的 Person 类中添加一个 printInfo() 函数,打印对象的数据:
class Person {
public:
Person(string n, Birthday b)
: name(n),
bd(b)
{
}
void printInfo()
{
cout << name << endl;
bd.printDate();
}
private:
string name;
Birthday bd;
};
请注意,我们可以调用 bd 成员的 printDate() 函数,因为它的类型是 Birthday,它定义了该函数。
组合3
现在我们已经定义了 Birthday 和 Person 类,我们可以去 main,创建一个 Birthday 对象,然后把它传递给一个 Person 对象。
int main() {
Birthday bd(7, 7, 1990);
Person p("Loen", bd);
p.printInfo();
}
/*输出
Loen
7/7/1990
*/
我们已经创建了一个日期为 7/7/1990 的生日对象。接下来,我们创建了一个 Person 对象,并将生日对象传递给它的构造函数。
最后,我们使用 Person 对象的 printInfo() 函数打印它的数据。
总的来说,组合可以让每个单独的类相对简单,直接,并专注于执行一项任务。
它还使每个子对象都是独立的,允许重用(我们可以在其他各种类中使用生日类)。
Friend 关键词
友元函数1
通常,类的 private 成员不能从该类以外的地方访问。
但是,声明一个非成员函数作为类的朋友允许它访问类的私有成员。这是通过在类中包含这个外部函数的声明来实现的,并且在关键字 friend 之前。在下面的例子中,someFunc() 不是类的成员函数,它是 MyClass 的一个好友,可以访问它的私有成员。
class MyClass {
public:
MyClass() {
regVar = 0;
}
private:
int regVar;
friend void someFunc(MyClass &obj);
};
请注意,在将对象传递给函数时,我们需要使用&运算符通过引用来传递它。
友元函数2
函数 someFunc() 被定义为类之外的常规函数??. 它将 MyClass 类型的对象作为参数,并能够访问该对象的私有数据成员。
class MyClass {
public:
MyClass() {
regVar = 0;
}
private:
int regVar;
friend void someFunc(MyClass &obj);
};
void someFunc(MyClass &obj) {
obj.regVar = 42;
cout << obj.regVar;
}
someFunc() 函数更改对象的私有成员并打印其值。
为了使其成员可访问,类必须在其定义中声明该函数为朋友。
你能把一个函数做为一个类的朋友,除非类“放弃”它对那个函数的友谊。
友元函数3
现在我们可以在 main 中创建一个对象,并调用 someFunc() 函数:
int main() {
MyClass obj;
someFunc(obj);
}
//输出 42
someFunc() 有权限修改对象的私有成员并打印它的值。
友元函数的典型用例是在访问两者的私有成员的两个不同类之间进行的操作。
你可以在任何数量的类中声明一个函数的朋友。
与友元函数类似,您可以定义一个朋友类,该类可以访问另一个类的私有成员。
This 关键词
This1
C++ 中的每个对象都可以通过称为 this 指针访问自己的地址。
在成员函数内部,这可以用来引用调用对象。
我们来创建一个示例类:
class MyClass {
public:
MyClass(int a) : var(a)
{ }
private:
int var;
};
友元函数没有这个指针,因为朋友不是一个类的成员。
This2
printInfo() 方法为打印类的成员变量提供了三种选择。
class MyClass {
public:
MyClass(int a) : var(a)
{ }
void printInfo() {
cout << var<<endl;
cout << this->var<<endl;
cout << (*this).var<<endl;
}
private:
int var;
};
所有三种选择将产生相同的结果。
this 是一个指向对象的指针,所以使用箭头选择操作符来选择成员变量。
This3
为了看到结果,我们可以创建我们的类的一个对象,并调用成员函数。
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int a) : var(a)
{ }
void printInfo() {
cout << var <<endl;
cout << this->var <<endl;
cout << (*this).var <<endl;
}
private:
int var;
};
int main() {
MyClass obj(45);
obj.printInfo();
}
/* Outputs
45
45
45
*/
所有这三种访问成员变量的方式都起作用。
请注意,只有成员函数有一个 this 指针。
This4
你可能想知道为什么有必要使用 this 关键字,当你有选择直接指定变量。
这个关键字在操作符重载中起着重要的作用,这将在下一课中介绍。
操作符重载
操作符重载1
大多数 C++ 内置操作符都可以重新定义或重载。
因此,操作符也可以与用户定义的类型一起使用(例如,允许您将两个对象添加在一起)。
这个图表显示了可以重载的操作符。

不能超载的运算符包括 :: | .* | . | ?:
操作符重载2
让我们声明一个示例类来演示运算符重载:
class MyClass {
public:
int var;
MyClass() {}
MyClass(int a)
: var(a) { }
};
我们的类有两个构造函数和一个成员变量。
我们将重载+运算符,以便将我们的类的两个对象添加到一起。
操作符重载4
重载操作符是由关键字 operator 定义的函数,后面跟随被定义操作符的符号。
重载操作符类似于其他函数,它具有返回类型和参数列表。
在我们的例子中,我们将重载+运算符。它会返回我们类的一个对象,并把我们类的一个对象作为参数。
class MyClass {
public:
int var;
MyClass() {}
MyClass(int a)
: var(a) { }
MyClass operator+(MyClass &obj) {
}
};
现在,我们需要定义函数的功能。
操作符重载5
我们需要我们的+操作符返回一个新的 MyClass 对象,其成员变量等于两个对象的成员变量之和。
class MyClass {
public:
int var;
MyClass() {}
MyClass(int a)
: var(a) { }
MyClass operator+(MyClass &obj) {
MyClass res;
res.var= this->var+obj.var;
return res;
}
};
在这里,我们声明了一个新的 res 对象。然后,我们将当前对象(this)和参数对象(obj)的成员变量的和赋值给 res 对象的 var 成员变量。
这使我们能够在 main 中创建对象,并使用重载的+运算符将它们添加在一起。
int main() {
MyClass obj1(12), obj2(55);
MyClass res = obj1+obj2;
cout << res.var;
}
//输出 67
随着运算符的重载,你可以使用任何自定义逻辑。但是,不可能改变运算符的优先级,分组或操作数的数量。
继承
继承1
继承是面向对象编程最重要的概念之一。
继承允许我们根据另一个类来定义一个类。 这有助于更轻松地创建和维护应用程序。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为(base)基类,新建的类称为(derived)派生类。
派生类继承了基类的所有特性,并且可以拥有自己的附加特性。

继承2
为了演示继承关系,我们通过创建一个Father类和Daughter类来进行演示。
class Father
{
public:
Father() {};
void sayHellow() {
cout << "hellow,W3Cschool";
}
};
class Daughter
{
public:
Daughter() {};
};
Father类中有一个sayHellow()的公共方法.
继承3
实例中通过Father类派生出Daughter类。
class Daughter : public Father
{
public:
Daughter() {};
};
通过:(冒号)加上public(访问说明符)可以指定基类,public代表基类中的所有公共成员在派生类中同样也是公共的。
我们可以理解为,Father类中的所有公共成员都成为了Daughter类的公共成员。
继承4
由于Father类中的所有公共成员都被Daughter类继承了。我们可以创建一个Daughter类型的对象,并通过该对象调用Father类中的sayHellow()函数。
#include <iostream>
using namespace std;
class Father
{
public:
Father() {};
void sayHellow() {
cout << "hellow,w3cschool";
}
};
class Daughter: public Father
{
public:
Daughter() {};
};
int main() {
Daughter d;
d.sayHellow();
}
//结果将会输出 "hellow,w3cschool"
派生类继承了所有的基类方法,但有以下几个例外:
基类的构造函数、析构函数和拷贝构造函数。
基类的重载运算符。
基类的友元函数。
通过逗号进行分隔可以让派生类指定多个基类。例如 狗:public 哺乳动物,public 犬科动物
Protected 成员
访问说明符
到目前为止,我们只使用了public和private访问说明符。
Public成员可以从类外任何地方访问,而private成员的访问仅限于类和友元函数。
正如我们以前所见,使用公共方法访问私有类变量是一个好习惯。
Protected
除此之外还有一个访问说明符 - protected。
protected的成员变量或函数与私有成员非常相似,唯一的区别就是 - 可以在派生类中访问它。
class Father {
public:
void sayHellow() {
cout << var;
}
private:
int var=0;
protected:
int someVar;
};
someVar可以被Father类的所有派生类访问,而var则不行。
继承的类型
访问说明符也被用来指定继承的类型。
注意,我们使用public来继承Father类:
class Daughter: public Father
继承的类型也支持private和protected
公共继承(Public Inheritance):基类的public成员成为派生类的public成员,基类的protected成员成为派生类的protected成员。 基类的private成员永远不能直接从派生类访问,但是可以通过调用基类的public和protected成员来访问。
受保护继承(Protected Inheritance):基类的public和protected成员成为派生类的受保护成员。
私有继承(Private Inheritance):基类的public和protected成员成为派生类的私有成员。
Public是最常用的继承类型,继承类型默认为Private
派生类的构造和析构函数
继承
当继承类时,基类的构造函数和析构函数不会被继承。
但是,当派生类的对象被创建或删除时,它们将被调用。
为了进一步解释这种行为,我们来创建一个包含构造函数和析构函数的示例类:
class Father {
public:
Father()
{
cout <<"Father构造函数"<<endl;
}
~Father()
{
cout <<"Father析构函数"<<endl;
}
};
在main中创建一个对象会产生一下的输出:
int main() {
Father f;
}
/* 输出
Father构造函数
Father析构函数
*/
继承
接下来,让我们创建一个Daughter类,使用它自己的构造函数和析构函数,并使其成为Father的派生类:
class Daughter: public Father {
public:
Daughter()
{
cout <<"Daughter构造函数"<<endl;
}
~Daughter()
{
cout <<"Daughter析构函数"<<endl;
}
};
继承
基于前面几题的基础上,当我们创建一个Daughter对象时会发生什么?(Daughter继承了Father,并且他们都声明了自己的构造函数与析构函数)
int main() {
Daughter f;
}
/*输出
Father 构造函数
Daughter 构造函数
Daughter 析构函数
Father 析构函数
*/
注意,首先调用基类的构造函数,然后调用派生类的构造函数。
当对象被销毁时,派生类的析构函数被调用,然后调用基类的析构函数。
可以理解为,派生类需要它的基类才能工作,这就是为什么基类是首先设置的原因。
概要
构造函数 - 首先调用基类的构造函数,然后调用派生类的构造函数。
析构函数 - 首先调用派生类的析构函数,然后调用基类的析构函数。
多态
多态1
多态按字面的意思就是多种形态。
当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
简单地说,多态意味着单个函数可以有多个不同的实现。
多态2
接下来我们将用例子来更清晰的了解多态。
假设我们现在要制作一个简单的游戏,游戏需要先创建一个角色,角色可以选择很多种不同的职业:法师,战士,射手等。但是这些职业有一个共同的功能就是攻击。不过由于职业性质的不同攻击的方式也会不一样,在这种情况下,多态允许在不同的对象上调用相同的攻击函数,但会导致不同的行为。
第一步是创建角色
class Role {
protected:
int attackPower;
public:
void setAttackPower(int a){
attackPower = a;
}
};
我们的Role类有一个名为setAttackPower的公共方法,它设置受保护的成员变量attackPower。
多态3
接下来我们为两个不同的职业,战士和法师创建类。这两个类都将继承Role类,所以他们都有attackPower(攻击力),但是他们又都有自己特定的攻击方式。
class Warrior: public Role{
public:
void attack() {
cout << "剑刃风暴! - "<<attackPower<<endl;
}
};
class Magician: public Role {
public:
void attack() {
cout << "冰暴! - "<<attackPower<<endl;
}
};
如上所示,他们的攻击方式各不相同。接下来我们准备创建我们的战士和法师的对象。
int main() {
Warrior w;
Magician m;
}
战士和法师都继承了Role类,所以战士和法师都是角色。所以我们可以实现以下的功能。
Role *r1 = &w;
Role *r2 = &m;
我们现在已经创建了两个Role类型的指针,指向战士和法师的对象。
多态
现在,我们可以调用相应的功能:
int main() {
Warrior w;
Magician m;
Role *r1 = &w;
Role *r2 = &m;
r1->setAttackPower(50);
r2->setAttackPower(80);
w.attack();
m.attack();
}
/* 输出:
剑刃风暴! - 50
冰暴! - 80
*/
通过直接在对象上调用函数,我们可以达到相同的效果。 但是,使用指针效率更高。
抽象函数
抽象函数1
前几节的例子演示了派生类与基类指针的使用方法。接下来我们接着之前游戏的例子,我们的每一个角色都有一个attack()函数。
为了能够让Role指针为每一个派生类提供调用attack()函数,我们需要在基类将函数声明成抽象函数。
在基类中声明一个抽象函数,在派生类中使用相应的函数,多态允许使用Role指针来调用派生类的函数。
每个派生类将覆盖attack()函数并有一个单独的实现:
class Role{
public:
virtual void attack() {
}
};
class Warrior: public Role {
public:
void attack() {
cout << "剑刃风暴!"<<endl;
}
};
class Magician: public Role {
public:
void attack() {
cout << "冰暴!"<<endl;
}
};
通过关键字virtual可以将基类的函数声明成抽象函数。
抽象函数2
现在,我们可以使用Role指针来调用attack()函数。
int main() {
Warrior w;
Magician m;
Role *r1 = &w;
Role *r2 = &m;
r1->attack();
r2->attack();
}
/* 输出:
剑刃风暴!
冰暴!
*/
由于attack()函数被声明为抽象的,它就像一个模板,告诉派生类自己有一个attack()函数。
抽象函数3
我们通过游戏的例子来演示多态的概念,我们使用Role指针调用相同的attack()函数,并生成不同的结果。
r1->attack();
r2->attack();
如果基类中的函数是抽象的,则派生类中的函数实现将根据所引用对象的实际类型进行调用,而不管原先声明的是那种类型。
声明或者继承了一个抽象函数的类被称为一个多态类。
抽象类
抽象函数
抽象函数也可以在基类中实现。
class Role {
public:
virtual void attack() {
cout << "角色!"<<endl;
}
};
class Warrior: public Role {
public:
void attack() {
cout << "战士!"<<endl;
}
};
class Magician: public Role {
public:
void attack() {
cout << "法师!"<<endl;
}
};
现在,当你创建一个Role指针,并调用attack()函数时,编译器会调用该指针指向的对应于该对象类型的函数:
int main() {
Warrior w;
Magician m;
Role r;
Role *r1 = &w;
Role *r2 = &m;
Role *r3 = &r;
r1->attack();
// 输出 "战士!"
r2->attack();
// 输出 "法师!"
r3->attack();
// 输出 "角色!"
}
纯虚函数1
在某些情况下,你希望在一个基类中包含一个抽象函数,以便它可以在派生类中被重新定义以适应该类的对象,但是没有有意义的定义可以给基类中的函数类。
没有定义的抽象成员函数被称为纯虚函数。他们指定派生类自己定义该函数。
语法是用= 0(一个等号和一个零)替换它们的定义:
class Role {
public:
virtual void attack() = 0;
};
纯虚函数2
一个纯虚函数基本上定义了派生类将自己定义的那个函数。
从具有纯虚拟函数的类继承的每个派生类必须重写该函数。
如果纯虚函数没有在派生类中重写,那么当您尝试实例化派生类的对象时,代码将无法编译并导致错误。
纯虚函数4
Role类中的纯虚函数必须在其派生类中重写。
class Role {
public:
virtual void attack() = 0;
};
class Warrior: public Role {
public:
void attack() {
cout << "战士!"<<endl;
}
};
class Magician: public Role {
public:
void attack() {
cout << "法师!"<<endl;
}
};
抽象类
你不能对一个有纯虚函数的基类创建对象。
下列例子将会报错。
Role r; // Role拥有一个纯虚函数,这样创建对象将会报错
这些类被称为抽象类。他们只能被当作基类使用,因此被允许具有纯虚函数。
你可能会认为抽象基类是无用的,但事实并非如此。 它可以用来创建指针并利用其的多态性质。
例如:
Warrior w;
Magician m;
Role *r1 = &w;
Role *r2 = &m;
r1->attack();
r2->attack();
函数模板
函数模板1
函数和类有助于使程序更易于编写,更安全,更易于维护。
然而,虽然函数和类确实具有所有这些优点,但在某些情况下,它们也可能受到C++规则的限制,必须为所有参数指定类型。
例如,你可能想写一个函数来计算两个数字的总和,就像这样:
int sum(int a, int b) {
return a+b;
}
函数模板2
我们现在可以在我们的main中调用两个整型函数。
int sum(int a, int b) {
return a+b;
}
int main () {
int x=6, y=16;
cout << sum(x, y) << endl;
}
// 输出 22
函数模板3
每一个新函数都需要声明类型,比如double型
double sum(double a, double b) {
return a+b;
}
如果能编写一个能处理任何类型参数的sum()函数,效率会不会更高呢?
函数模板使我们有能力做到这一点!
使用函数模板的基本思想是避免为每个变量指定确切的类型。相反,C ++为我们提供了使用占位符类型(称为模板类型参数)来定义函数的功能。
要定义函数模板,请使用关键字template,然后使用template类型定义:
template <class T>
函数模板4
现在我们可以在函数中使用我们的通用数据类型T:
template <class T>
T sum(T a, T b) {
return a+b;
}
int main () {
int x=6, y=16;
cout << sum(x, y) << endl;
}
// 输出 22
该函数返回一个泛型类型T的值,获取的两个参数也是类型T。
函数模板5
其他数据类型可以使用相同的函数,例如double型:
template <class T>
T sum(T a, T b) {
return a+b;
}
int main () {
double x=7.02, y=15.64;
cout << sum(x, y) << endl;
}
// 输出 22.66
编译器自动调用相应类型的函数。
在创建模板类型参数时,可以使用关键字typename替代类型关键字。
class:template <typename T>
函数模板6
函数模板可以节省很多时间,因为它们只写一次,但是可以被不同类型调用。
函数模板减少了代码维护,因为重复代码显着减少。
增强安全性是使用函数模板的另一个优点,因为不需要手动复制函数和更改类型。
带多个参数的函数模板
函数模板1
函数模板还可以处理多个通用数据类型。 使用逗号分隔需定义的数据类型。
让我们来创建一个包含多种不同数据类型的参数(一个int和一个double)的函数,然后打印一个较小的值。
template <class T, class U>
正如你所看到的,这个模板声明了两个不同的通用数据类型T和U.
函数模板2
现在我们可以继续我们的函数声明:
template <class T, class U>
T smaller(T a, U b) {
return (a < b ? a : b);
}
(a < b ? a : b)三元运算符用法,检查a<b,如果条件成立返回a,否则返回b
函数模板3
在main中,我们可以使用不同数据类型的函数:
template <class T, class U>
T smaller(T a, U b) {
return (a < b ? a : b);
}
int main () {
int x=70;
double y=69.99;
cout << smaller(x, y) << endl;
}
// 输出 69
最后输出是int型的,应为我们在调用函数模板的时候声明了int型函数。
函数模板4
上述例子中的T仅仅只是一个参数名,type的缩写。在定义参数的时候你可以取一个你自己适用的名字。
但是要记住,当你定义了一个模板参数后,你必须在函数定义中使用它,不然编译器会报错。
模板类
类模板1
就像我们可以定义函数模板一样,我们也可以定义类模板,允许类使用模板参数的成员作为类型。
使用相同的语法来定义类模板:
template <class T>
class MyClass {
};
就像函数模板一样,您可以使用逗号分隔的列表来定义多个通用数据类型。
类模板2
我们创建一个类Pair,它将保存一对通用类型的值。
template <class T>
class Pair {
private:
T first, second;
public:
Pair (T a, T b):
first(a), second(b) {
}
};
上面的代码声明了一个类模板Pair,带有两个泛型类型的私有变量,以及一个用于初始化变量的构造函数。
类模板3
如果您在类的外部定义成员函数,则需要使用特定的语法,例如在单独的源文件中。
您需要在类名后面的尖括号中指定泛型类型。
例如,要在类的外部定义一个成员函数bigger(),使用以下语法:
template <class T>
class Pair {
private:
T first, second;
public:
Pair (T a, T b):
first(a), second(b){
}
T bigger();
};
template <class T>
T Pair<T>::bigger() {
// 此处省略
}
类模板4
bigger()函数将返回两个成员变量的最大值。
template <class T>
class Pair {
private:
T first, second;
public:
Pair (T a, T b):
first(a), second(b){
}
T bigger();
};
template <class T>
T Pair<T>::bigger() {
return (first>second ? first : second);
}
类模板5
要创建不同类型的模板类的对象,请在尖括号中指定数据类型,就像我们在定义类之外的函数时所做的那样。
在这里,我们为整数创建一个Pair对象。
Pair <int> obj(11, 22);
cout << obj.bigger();
// 输出 22
我们也可以使用相同的类来定义一个double型的对象
Pair <double> obj(23.43, 5.68);
cout << obj.bigger();
// 输出 23.43
模板特化
异常处理
处理用户输入操作时,异常处理特别有用。
例如,一个要求用户输入两个数字的程序,然后进行整除,你必须对0进行限制,防止用户在第二个数字的时候输入0。
int main() {
int num1;
cout <<"输入你的第一个数字";
cin >> num1;
int num2;
cout <<"输入你的第二个数字";
cin >> num2;
cout <<"Result:"<<num1 / num2;
}
如果用户输入除0之外的任何数字,则该程序正常工作。
但是如果输入了0则会导致程序崩溃,所以我们需要对输入的数据进行处理。
模板特化
要为数据类型char指定不同的行为,我们将创建一个模板特化。
template <class T>
class MyClass {
public:
MyClass (T x) {
cout <<x<<"不是char类型"<<endl;
}
};
template < >
class MyClass<char> {
public:
MyClass (char x) {
cout <<x<<"是char类型!"<<endl;
}
};
首先,请注意,我们在模板<>的前面添加了一个空的参数列表。 这是因为所有类型都是已知的,并且这个特化不需要模板参数,但是仍然是类模板的特化,因此需要注意这一点。
但比这个前缀更重要的是类模板名称之后的特化参数。 这个特化参数本身标识了模板类被特化的类型(char)。
在上面的例子中,第一个类是通用模板,第二个是特化。
模板特化
下一步是声明不同类型的对象并检查结果:
int main () {
MyClass<int> ob1(28);
MyClass<double> ob2(5.18);
MyClass<char> ob3('w3cschool');
}
/* 输出:
28 - 不是char类型
5.18 - 不是char类型
w3cschool 是char类型!
*/
正如你所看到的,泛型模板为int和double所调用。 但是,我们的模板特化是为char数据类型调用的。
请记住,从泛型模板到特化没有成员“继承”,所以模板类特化的所有成员都必须自行定义。
异常
异常
程序执行过程中出现的问题称为异常。
在C ++中,异常是程序运行时产生的错误的反应,例如试图除以零。
抛出异常
C ++异常处理基于三个关键字:try,catch和throw。
当问题出现时throw是用来抛出异常的。
int fatherAge = 18;
int sonAge = 38;
if (sonAge > fatherAge) {
throw "你爸真年轻~";
}
上述代码中,当sonAge比fatherAge大的时候就会抛出异常。
在throw语句中,参数确定异常的类型。 这可以是任何表达式。 表达式结果的类型将决定抛出异常的类型。
捕捉异常
try标识将激活用于检测异常的代码块。 接下来是一个或多个catch块。 catch关键字表示在引发特定异常时执行的代码块。
可以生成异常的代码被try / catch块包围。
您可以通过设置关键字catch后面的括号中显示的异常声明来捕获哪种类型的异常。
try {
int fatherAge = 18;
int sonAge = 38;
if (sonAge > fatherAge) {
throw 666;
}
}
catch (int x) {
cout<<"你确定这是你爸?"<<x;
}
//输出 "你确定这是你爸? 666"
try块引发异常,然后catch块处理它。
错误代码666是一个整数,它出现在throw语句中,所以它会导致int类型的异常。
可能会列出多个catch语句来处理各种异常,以防try块引发多个异常。
异常处理
异常处理1
处理用户输入时,异常处理特别有用。
例如,一个程序要求用户输入两个数字,然后进行整除,为了确保被除数不为0,你就需要用异常处理。
int main() {
int num1;
cout <<"输入第一个数字:";
cin >> num1;
int num2;
cout <<"输入第二个数字:";
cin >> num2;
cout <<"结果:"<<num1 / num2;
}
如果用户输入除0外的任何数字,程序将正常运行。
但是当用户在第二个数字的时候输入了0,则程序会崩溃
异常处理2
如果第二个数字等于0,我们需要抛出异常。
int main() {
int num1;
cout <<"输入第一个数字:";
cin >> num1;
int num2;
cout <<"输入第二个数字:";
cin >> num2;
if(num2 == 0) {
throw 0;
}
cout <<"结果:"<<num1 / num2;
}
如果第二个数字输入为0,这段代码将会抛出一个int型的异常:0。
异常处理3
现在我们需要使用try / catch块来处理抛出的异常。
int main() {
try {
int num1;
cout <<"输入第一个数字:";
cin >> num1;
int num2;
cout <<"输入第二个数字:";
cin >> num2;
if(num2 == 0) {
throw 0;
}
cout <<"结果:"<<num1 / num2;
}
catch(int x) {
cout <<"请勿输入0!";
}
}
上述代码将会避免出现当num2为0时导致程序崩溃的问题,同时会抛出异常。
在我们的例子中,我们只捕获整数类型的异常。 可以指定你的catch块处理在try块中抛出的任何类型的异常。可以通过在catch的括号之间添加一个省略号(...)实现。
try {
// 要执行的代码
} catch(...) {
// 用来处理异常的代码
}
引入文件
引入文件
另一个有用的C++功能是读取和写入文件的能力。这需要标准的C++库fstream。
fstream中定义了三种新的数据类型:
ofstream:输出文件流,创建信息并将其写入文件。
ifstream:输入从文件中读取信息的文件流。
fstream:一般文件流,具有ofstream和ifstream功能,允许它创建,读取和写入信息到文件。
要在C++中执行文件处理,头文件和必须包含在C ++源文件中。
#include <iostream>
#include <fstream>
打开文件
必须先打开一个文件,然后才能读取或写入文件。
可以使用ofstream或fstream对象来打开文件进行写入。
我们打开一个名为“w3cschool.txt”的文件,并写下一些内容:
#include <iostream>
#include <fstream>
using namespace std;
int main() {
ofstream MyFile;
MyFile.open("w3cschool.txt");
MyFile << "hellow W3cSchool";
}
上面的代码创建一个名为MyFile的流对象,并使用open()函数在文件系统上打开“w3cschool.txt”文件。 如您所见,使用相同的流输出操作符来写入文件。
如果指定的文件不存在,打开的功能会自动创建。
关闭文件
当你完成对一个文件的操作后,可以使用成员函数close()关闭它。
#include <iostream>
#include <fstream>
using namespace std;
int main() {
ofstream MyFile;
MyFile.open("w3cschool.txt");
MyFile << "hellow W3cSchool";
MyFile.close();
}
更多关于引入文件的介绍
引入文件1
您也可以使用ofstream对象的构造函数直接提供文件的路径并打开,而不是调用open函数。
#include <fstream>
using namespace std;
int main() {
ofstream MyFile("w3cschool.txt");
MyFile << "哇!666666";
MyFile.close();
}
引入文件2
在某些情况下,例如当您没有文件权限时,可能会打开失败。
is_open()成员函数检查文件是否可以被访问。
#include <iostream>
#include <fstream>
using namespace std;
int main() {
ofstream MyFile("w3cschool.txt");
if (MyFile.is_open()) {
MyFile << "hellow W3cschool";
}
else {
cout << "抱歉,文件无法访问";
}
MyFile.close();
}
文件打开模式
可以通过设置open函数的第二个参数定义打开文件的模式。

可以通过"|"设定多个模式。
例如,要以写入模式打开文件并截断它(如果已经存在),请使用以下语法:
ofstream outfile;
outfile.open("file.dat", ios::out | ios::trunc );
从文件读取
您可以使用ifstream或fstream对象从文件读取信息。
#include <iostream>
#include <fstream>
using namespace std;
int main () {
string line;
ifstream MyFile("w3cschool.txt");
while ( getline (MyFile, line) ) {
cout << line << '\n';
}
MyFile.close();
}
getline函数从输入流中读取字符并将其放入字符串中。