C++基础

在Ubuntu 下编写C++

本章节内容主要介绍在Ubuntu 在终端窗口下使用vi/vim 编辑一个C++源文件。通过编写最简单的示例“Hello,World!”。带领大家学习如何在Ubuntu 终端下编辑和编译C++。这里要求大家会在Ubuntu 上使用vi/vim,也就是要求大家有一点Ubuntu 入门的基础。如果没有这些基础也是可以拷贝C++的代码到Windows 上使用像Dev-C++这种轻量级C/C++ 集成开发环境(IDE)进行编写和编译。

但是需要学习在Ubuntu 下编写C++,因为后面第二章的内容都是在Ubuntu 下编写和讲解C++的基础。同时也可以锻炼在Linux 开发C++的能力。

C++简介

C++ (c plus plus)是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,支持过程化编程面向对象编程泛型编程。C++ 被认为是一种中级语言,它综合了高级语言和低级语言的特点。C++ 是由Bjarne Stroustrup 于1979 年在新泽西州美利山贝尔实验室开始设计开发的。C++ 进一步扩充和完善了C 语言,最初命名为带类的C,后来在1983 年更名为C++。C++ 是C 的一个超集,事实上,任何合法的C 程序都是合法的C++ 程序。

2017 年发布C++17,已经是第五个C++标准了。我们也见过或者听过C++98,这样的C++标准,也就是1998 年发布的C++,所以叫C++98,是C++的第一个标准。

学习C++我们要理解概念,而不是深究语言技术细节。我们只要带着第二章的C++基础概
念,学习Qt 或者写C++会有一定的帮助。

C++环境设置

为了写这份教程,作者也是从新装了一个Ubuntu18.04,从头搭建环境。
我们先配置软件源的服务器地址为阿里云的地址。这样我们可以从国内去获取软件源,下载速度会更快。
我们要在Ubuntu 编写C++程序,那么需要有能编写代码的文本编辑和C++编译器。在新
装的Ubuntu 环境里,编译C 语言的GCC 没有安装,编译C++的G++也没有安装。执行下面的执指令安装编译C 语言和C++的环境。

sudo apt-get install gcc g++
sudo apt-get install lsb-core lib32stdc++6 // 安装其他库

安装完成后,可以使用下面的指令来查看安装的gcc 和g++的版本。

g++ -v
gcc -v

在这里插入图片描述

编写一个简单的C++程序

使用vi 指令编辑01_hello_world.cpp。

mkdir C++ 				// 创建一个C++目录。
cd C++ 					// 进入创建的C++目录。
mkdir 01_hello_world 	// 创建一个01_hello_world 目录
cd 01_hello_world 		// 进入01_hello_world 目录下。
vi 01_hello_world.cpp 	// 编辑cpp 文件,拷贝下文的内容

在这里插入图片描述
拷贝下面的内容到01_hello_world.cpp。

#include <iostream>
using namespace std;
int main()
{
	cout << "Hello, World!" << endl;
	return(0);
}
  • 第1 行,C++ 语言定义了一些头文件,这些头文件包含了程序中必需的或有用的信息。上面这段程序中,包含了头文件。
  • 第2 行,using namespace std; 告诉编译器使用std 命名空间。命名空间是C++ 中一个相对新的概念。其中std 就是C++里的标准命名空间,也就是标准库里写好的了,我们可以直接调用。
  • 第3 行,int main() 是主函数,程序从这里开始执行。
  • 第5 行,cout << “Hello World”<<endl; 会在屏幕上显示消息"Hello World"并换行。“<<”
    是运算符,endl 是换行语句。
  • 第6 行,return 0; 终止main( )函数,并向调用进程返回

执行下面的语句进行编译和运行这个简单的C++程序。

g++ 01_hello_world.cpp -o 01_hello_world // 使用g++编译。-o 后面加的是输出的目标文件。
./01_hello_world // 在终端下执行,打印"Hello, World!"并换行。

在这里插入图片描述

我们可以拓展一下,如何输出多行。可以像下面一样无限加下去。其中我们发现打印了第一个Hello,world!后也换行了,因为使用了“\n”。C++中可以使用C 语言的语句,C++是C 语言的超集。

#include <iostream>
using namespace std;
int main()
{
	cout << "Hello, world!\n" << "Hello, world!" << endl;
	return(0);
}

第5 行,我们在里面再加用“<<”插入运算符(重载运算符)再插入一句“Hello, world!”打印,这样终端上就打印了两行“Hello, world!”。

在第二章C++基础里,这里主要介绍概念为主,主要介绍C++与C 语言中常用的不同点,和一些新的变化。其中不会去说指针、数据类型、变量类型、判断和循环等这些知识,这些和C 语言基本是一样使用的。

我们主要学习C++的面向对象编程,对学习Qt 有很大的帮助,理解第2.2 章节的概念很重要。Qt 里就能体现到C++编程带来的优势和便处。就算没学过C++,学习Qt 也不会很难。写C++基础这章,笔者已经把重要的概念写出来,但是实际上C++的内容不止这么多,第二章是快餐式C++入门,主要是为了更好的理解Qt 中的C++语法,学习Qt时也方便理解其中的内容。

C++的新特性

C++比C 语言新增的数据类型是布尔类型(bool)。但是在新的C 语言标准里已经有布尔类型了,但是在旧的C 语言标准里是没有布尔类型的,编译器也无法解释布尔类型。

在传统的 C 语言里,变量初始化时必须在程序的前面定义在前面,而C++则是可以随用随定义。C++也可以直接初始化,比如int x(100);这样就直接赋值x=100,这些都是C++特性的好处。这里只说这些常用的新特性,其他特性不做描述或者解释了。

C++的输入输出方式

在C 语言里,我们是这样输入或者输出的。

在这里插入图片描述

在C++里,我们使用以cin 和cout 代替了scanf 和printf。在输入和输出的流程上是不变的,只是关键字变了,用法也变了。

在这里插入图片描述
要说效率上,肯定是C 语言的scanf 和printf 的效率高,但是没有C++中的cin 和cout 使用方便。

C++的I/O 语法方式如下。
cout 语法形式:

cout<<x<<endl;

x 可以是任意数据类型,甚至可以写成一个表达式,这比C 语言需要指定数据类型方便多了,endl 指的是换行符,与C 语言的“\n”效果一样。

错误示例:

cout<<x,y<<endl; // 在变量间不能用“,”。

正确写法:

cout<<x<<y; // endl 可流省略,只是一个换行的效果。

cin 语法形式:

cin>>x;

x 可以是任意数据类型。
拓展,如何输入两个不同的变量。

cin>>x>>y;

C++ 之命名空间 namespace

在第1.3 小节里我们已经使用过命名空间,如下代码第2 行。using namespace std;同时我们要注意第1 行,不能写成iostream.h,有.h 的是非标准的输入输出流,c 的标准库。无.h 的是标准输入输出流就要用命名空间。

#include <iostream>
using namespace std;
int main()
{
	cout << "Hello, World!" << endl;
	return(0);
}

using 是编译指令,声明当前命名空间的关键词。可以从字面上理解它的意思,using 翻译成使用。这样可以理解成使用命名空间std。因为cin 和cout 都是属于std 命名空间下的东西,所以使用时必须加上using namespace std;这句话。cin 和cout 可以写std::cin 和std::cout,“::”表示作用域,cin 和cout 是属于std 命名空间下的东西,这里可以理解成std 的cin 和std 的cout。

为什么要使用命名空间?

有些名字容易冲突,所以会使用命名空间的方式进行区分,具体来说就是加个前缀。比如C++ 标准库里面定义了vector 容器,您自己也写了个vector 类,这样名字就冲突了。于是标准库里的名字都加上std:: 的前缀,您必须用std::vector 来引用。同理,您自己的类也可以加个自定义的前缀。但是经常写全名会很繁琐,所以在没有冲突的情况下您可以偷懒,写一句using namespace std;,接下去的代码就可以不用写前缀直接写vector 了。

从命名空间开始我们就隐隐约约可以看到C++面向对象的影子了。命名空间在很多C++库里使用到。有些公司也会自定义自己的C++库,里面使用了大量的命名空间。从这里我们也可以看出C++是非常之有条理的,容易管理的,不含糊,易使用的。

在初学Qt 时我们是比较少使用命名空间,或者比较少看到命名空间。当然也是可以在Qt里自定义命名空间,然后与C++一样正常使用。

下面通过一个简单的例子来介绍自定义的命名空间和使用自定义的命名空间。在Ubuntu上我们新建一个目录02_namespace_example,然后在02_namespace_example 里新建一个02_namespace_example.cpp 文件,内容如下。

#include <iostream>
using namespace std;

namespace A
{
	int x = 1;
	void fun()
	{
		cout << "A namespace" << endl;
	}
}
using namespace A;
int main()
{
	fun();
	A::x = 3;
	cout << A::x << endl;
	A::fun();
	return(0);
}

第4 行,自定义了命名空间A,里面定义了一个变量x,并将x 赋值为1;定义了一个函数
fun(),并在fun()加了输出打印语句cout<<“A namespace”<<endl;。
第11 行, 声明使用命名空间A。
第14 行,在第11 行声明了命名空间A 后,才能直接使用fun();否则要写成A::fun();
第15 行,将A 命名空间下的x 重新赋值为3。
第16 行,打印出A 命名空间下的x 的值。
第17 行,调用A 命名空间下的fun()。

执行下面的指令开始编译。

g++ 02_namespace_example.cpp -o 02_namespace_example

编译完成执行的结果如下:

在这里插入图片描述

C++面向对象

面向对象的三大特征是继承多态封装,C++重面向对象重要的就是这些。

类和对象

C++ 在C 语言的基础上增加了面向对象编程,C++ 支持面向对象程序设计。类是C++的核心特性,通常被称为用户定义的类型。类用于指定对象的形式,它包含了数据表示法和用于处理数据的方法。类中的数据和方法称为类的成员。函数在一个类中被称为类的成员。

打个比方说明一下什么是,比如有一条小狗,小狗有名字叫旺财,旺财的年龄是2 岁,同时旺财会汪汪的叫,也能跑。我们统称狗这个为类,类是我们抽象出来的,因为狗不只有上面的属性,还有体重,毛发的颜色等等,我们只抽象出几种属性成一个类。

具体到哪条狗就叫对象

从类中实例化对象分两种方法,一种是从中实例化对象,一种是从中实例化对象。

下面以自定义狗类介绍如何自定义类和如何使用对象。

新建一个03_class_dog_example.cpp 文件,内容如下。

#include <iostream>
#include <string>
using namespace std;

class Dog
{
	public:
		string	name;
		int	age;
	
		void run()
		{
			cout << "小狗的名字是:" << name << "," << "年龄是" << age << endl;
		}
};

int main()
{
	Dog dog1;

	dog1.name	= "旺财";
	dog1.age	= 2;
	dog1.run();

	Dog *dog2 = new Dog();

	if ( NULL == dog2 )
	{
		return(0);
	}
	dog2->name	= "富贵";
	dog2->age	= 1;
	dog2->run();


	delete dog2;
	dog2 = NULL;
	return(0);
}

第5 行,定义了一个Dog 狗,定义类时,起的类名要尽量贴近这个类,让人一看就明白。

第7 行,访问限定符public(公有的),此外还有private(私有的)和protected(受保护的)。写这个的目的是为了下面我们要调用这些成员,不写访问限定符默认是private。访问限定符就是设置一个成员变量和成员函数的访问权限。

第8 至11 行,定义了一个字符串变量name,整形变量age。和一个方法run()。我们在这个run()里打印相应的狗名和狗的年龄。PS:string 是C++的数据类型,方便好用,使用频率相当高。

第18 行,从栈中实例化一个对象dog1(可以随意起名字)。

第20 至22 行,为dog1 的成员变量赋值,dog1 的name 赋值叫“旺财”,年龄为2 岁。然后调用run()方法,打印dog1 的相关变量的信息。

第24 行,从堆中实例化对象,使用关键字new 的都是从堆中实例化对象。

第26 行,从堆中实例化对象需要开辟内存,指针会指向那个内存,如果new 没有申请内存成功,p 即指向NULL,程序就自动退出,下面的就不执行了,写这个是为了严谨。

第29 至31 行,和dog1 一样,为dog2 的成员赋值。

第34 和35 行,释放内存,将dog2 重新指向NULL。

如果没有语法错误,我们完全可以预测到打印的结果。我们学习 C 语言的结构体,类其实和结构类似,可以说类是结构体的升级版本。

执行下面的指令开始编译。

g++ 03_class_dog_example.cpp -o 03_class_dog_example

编译完成后执行的结果如下。

在这里插入图片描述

通过上面的例子我们已经学习了什么是类,什么是对象。以描述Dog 为一类(抽象出来的),从Dog 类中实例出来就是对象(实际事物)。对象拥有Dog 类里的属性,可以从栈中实例化对象,页可从堆中实例化对象。

构造函数与析构函数

构造函数在对象实例化时被系统自动调用,仅且调用一次。实际上定义类时,如果没有定义构造函数和析构函数,编译器就会生成一个构造函数和析构函数,只是这个构造和析构函数什么事情也不做。

构造函数的特点如下:
(1)构造函数必须与类名同名;
(2)可以重载(重载?新概念,后面学到什么是重载。);
(3)没有返回类型,即使是void 也不行。


什么是析构函数?与构造函数相反,在对象结束其生命周期时系统自动执行析构函数。实
际上定义类时,编译器会生成一个析构函数。

析构函数的特点如下:
(1)析构函数的格式为~类名();
(2)调用时释放内存(资源);
(3)~类名()不能加参数;
(4)没有返回值,即使是void 也不行。

下面我们通过简单的例子来说明构造函数和析构函数的使用。

#include <iostream>
#include <string>
using namespace std;

class Dog
{
	public:
		Dog();	// 构造函数
		~Dog();	// 析构函数
};

int main()
{
	Dog dog;
	cout << "构造与析构函数示例" << endl;
	return(0);
}


Dog::Dog()
{
	cout << "构造函数执行!" << endl;
}


Dog::~Dog()
{
	cout << "析构函数执行!" << endl;
}

我们还是以简单的狗类作为示例,定义一个狗类,把构造函数和析构函数写上。前面不是说会自动生成构造函数和析构函数的吗?注意是编译时,编译器生成的。当我们要使用构造函数和析构函数时需要我们自己在类里添加。

第5 至第10 行,定义了一个狗类,并在里面写了构造函数和析构函数。
第14 行,使用Dog 类实例化一个dog 对象。
第15 行,打印一句"构造与析构函数示例"。
第19 至22 行,类的函数可以在类里实现,也可以在类外实现,不过在类外实现时需要使
用“::”,此时我们把类的构造函数定义在类的外面,打印一句"构造函数执行!“。
第14 至27 行,类的析造函数定义在类的外面,打印一句"析造函数执行!”。

执行下面的指令开始编译。

g++ 04_structor_example.cpp -o 04_structor_example

编译完成后执行的结果如下。

在这里插入图片描述

其实执行的结果也是可以预测的,在对象实例化时调用构造函数,所以Dog()先执行,然后再在main()函数里继续执行cout<<“构造与析构函数示例”<<endl;。最后对象生命周期结束时调用析构函数

this 指针

每个对象都拥有一个this 指针,this 指针记录对象的内存地址。

在C++中,this 指针是指向类自身数据的指针,简单的来说就是指向当前类的当前实例对象。

关于类的this 指针有以下特点:
(1)this 只能在成员函数中使用,全局函数、静态函数都不能使用this。实际上,成员函数默认第一个参数为 T * const this。也就是一个类里面的成员了函数 int func(int p),func 的原型在编译器看来应该是 int func(T * const this,int p)。
(2)this 在成员函数的开始前构造,在成员函数的结束后清除。
(3)this 指针会因编译器不同而有不同的放置位置。可能是栈,也可能是寄存器,甚至全局变量。

下面以简单的例子来说明this 的用法。

我们还是以狗类为例,按上面的 this 解释,this 只能够在成员函数使用,并可以指向自身数据。我们就可以写这样简单的例子来说明this 的用法。

我们在Qt 里也会遇到this 这个东西,下面这个例子就很容易解释Qt 里的this 指针的用法。
新建一个目录05_this_pointer_example,编辑一个05_this_pointer_example.cpp 内容如下。

#include <iostream>
#include <string>
using namespace std;

class Dog
{
	public:
		string name;
		void func();
};

int main()
{
	Dog dog;
	dog.func();
	return(0);
}


void Dog::func()
{
	// 在类的成员函数里使用了this 指针
	this->name = "旺财";
	cout << "小狗的名字叫:" << this->name << endl;
}

第21 和22 行,在类的成员函数里使用了this 指针,并指向了类里的成员name。先将name赋值叫“旺财”,然后我们打印name 的值。

当程序没有语法错误里我们可以预测打印的结果,就是“小狗的名字叫:旺财”。
执行下面的指令开始编译。

g++ 05_this_pointer_example.cpp -o 05_this_pointer_example

程序执行的结果如下。

在这里插入图片描述

继承(基类和派生类)

面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。

当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。在Qt 里大量的使用了这种特性,当Qt 里的类不满足自己的要求时,我们可以重写这个类,就是通过继承需要重写的类,来实现自己的类的功能。

一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:

class derived-class: access-specifier base-class

与类的访问修饰限定符一样,继承的方式也有几种。其中,访问修饰符access-specifier 是public、protected 或private 其中的一个,base-class 是之前定义过的某个类的名称。如果未使用访问修饰符access-specifier,则默认为private。

下面来捋一捋继承的方式,例子都是以公有成员和公有继承来说明,其他访问修饰符和其他继承方式,大家可以在教程外自己捋一捋。这个公有成员和继承方式也没有什么特别的,无非就是不同的访问权限而已,可以这样简单的理解。

  1. 公有继承(public):当一个类派生继承公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
  2. 保护继承(protected):当一个类派生继承保护基类时,基类的公有和保护成员将成为派生类的保护成员。
  3. 私有继承(private):当一个类派生继承私有基类时,基类的公有和保护成员将成为派生类的私有成员。

下面我们还是以狗类为例,在2.2.1 小节里我们定义的狗类,已经定义了name,age 和run()方法。假设我们不想重写这个狗类,而是新建一个Animal 类,让狗类去继承这个Animal 类。

假设是公有继承,那么我们是不是可以在狗类实例的对象里去使用继承Animal 类里的成员呢?

编辑一个06_inherit_example.cpp 内容如下。

#include <iostream>
#include <string>
using namespace std;

/*动物类,抽象出下面两种属性,
 * *颜色和体重,是每种动物都具有的属性
 */
class Animal
{
	public:
		/* 颜色成员变量*/
		string color;
		/* 体重成员变量*/
		int weight;
};
/*让狗类继承这个动物类,并在狗类里写自己的属性。
 * *狗类拥有自己的属性name,age,run()方法,同时也继承了
 * *动物类的color和weight的属性
 */
class Dog : public Animal
{
	public:
		string	name;
		int	age;
		void run();
};

int main()
{
	Dog dog;
	dog.name	= "旺财";
	dog.age		= 2;
	dog.color	= "黑色";
	dog.weight	= 120;
	cout << "狗的名字叫:" << dog.name << endl;
	cout << "狗的年龄是:" << dog.age << endl;
	cout << "狗的毛发颜色是:" << dog.color << endl;
	cout << "狗的体重是:" << dog.weight << endl;
	return(0);
}

Animal 作为基类,Dog 作为派生类。Dog 继承了Animal 类。访问修饰符为public(公有继承)。

执行下面的指令开始编译。

g++ 06_inherit_example.cpp -o 06_inherit_example

编译完成执行的结果为如下。

在这里插入图片描述

重载

C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。

重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。

当您调用一个重载函数或重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策。

函数重载(同名函数形参不同)

在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。我们不能仅通过返回类型的不同来重载函数。在Qt源码里,运用了大量的函数重载,所以我们是有必要学习一下什么是函数重载。不仅在C++,在其他语言的里,都能看见函数重载。因为需要不同,所以有重载各种各样的函数。

下面通过一个小实例来简单说明一下函数重载的用法。我们还是以狗类为说明,现在假设有个需求。我们需要打印狗的体重,分别以整数记录旺财的体重和小数记录旺财的体重,同时以整数打印和小数打印旺财的体重。那么我们可以通过函数重载的方法实现这个简单的功能。

编辑一个07_func_overloading.cpp 内容如下。

#include <iostream>
#include <string>
using namespace std;

class Dog
{
	public:
		string name;
		void getWeight( int weight )
		{
			cout << name << "的体重是:" << weight << "kG" << endl;
		}
	
	
		void getWeight( double weight )
		{
			cout << name << "的体重是:" << weight << "kG" << endl;
		}
};

int main()
{
	Dog dog;
	dog.name = "旺财";
	dog.getWeight( 10 );
	dog.getWeight( 10.5 );
	return(0);
}

第9 行,写了一个方法getWeight(int weight),以int 类型作为参数。
第13 行,以相同的函数名getWeight,不同的参数类型double weight,这样就构成了函数重载。
第22 行与第23 行,分别传进参数不同的参数,程序就会匹配不同的重载函数。

执行下面的指令编译。

g++ 07_func_overloading.cpp -o 07_func_overloading

程序执后的结果如下。

在这里插入图片描述

通过上面的例子我们可以知道重载函数的使用方法,避免用户传入的参数类型,有可能用户传入的参数类型不在我们写的重载函数里,假若用户传入了一个字符串类型,这样编译器就会匹配不到相应的重载函数,编译时就会报错。其实我们还可以多写几个重载函数,设置多几种类型,如string 类型,char 类型,float 类型等。

运算符重载

运算符重载的实质就是函数重载或函数多态。运算符重载是一种形式的C++多态。目的在于让人能够用同名的函数来完成不同的基本操作。要重载运算符,需要使用被称为运算符函数的特殊函数形式,运算符函数形式:operatorp(argument-list),operator 后面的’p’为要重载的运算符符号。重载运算符的格式如下:

<返回类型说明符> operator <运算符符号>(<参数表>)
{
	<函数体>
}

下面是可重载的运算符列表:
在这里插入图片描述
在这里插入图片描述

下面是不可重载的运算符列表:

在这里插入图片描述

根据上表我们知道可以重载的运算符有很多,我们以重载“+”运算符为例,实际上用重载运算符我们在实际应用上用的比较少,我们只需要了解和学习这种思想即可。

下面的实例使用成员函数演示了运算符重载的概念。在这里,对象作为参数进行传递,对象的属性使用this 运算符进行访问。下面还是以我们熟悉的狗类为例。声明加法运算符用于把两个Dog 对象相加的体重相加,返回最终的Dog 对象然后得到第三个Dog 对象的体重。
新建一个目录08_operator_example,编辑一个08_operator_example.cpp 内容如下。

#include <iostream>
#include <string>
using namespace std;

class Dog
{
	public:
		int weight;
		Dog operator+( const Dog &d )
		{
			Dog dog;
			dog.weight = this->weight + d.weight;
			return(dog);
		}
};

int main()
{
	Dog	dog1;
	Dog	dog2;
	Dog	dog3;
	dog1.weight	= 10;
	dog2.weight	= 20;
	dog3		= dog1 + dog2;
	cout << "第三只狗的体重是:" << dog3.weight << endl;
	return(0);
}

第9 至13 行,重载“+”运算符,注意函数必须与类名同名,把Dog 对象作为传递,使用this 运算符进行访问。然后返回一个dog 对象。

执行下面指令进行编译。

g++ 08_operator_example.cpp -o 08_operator_example

编译完成后运行的结果如下。

在这里插入图片描述

结果可以预知的,重载运算符“+”,可以把两个对象进行相加。在普通的算术运算符“+”是不能将两个对象进行相加的,所以我们重载运算符的意义可以体现在这里。

多态

C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数;

形成多态必须具备三个条件:

  1. 必须存在继承关系;
  2. 继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字virtual 声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数);
  3. 存在基类类型的指针或者引用,通过该指针或引用调用虚函数。

这里我们还需要理解两个概念:

虚函数

是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。虚函数声明如下:

virtualReturnType FunctionName(Parameter) 

虚函数必须实现,如果不实现,编译器将报错

纯虚函数

若在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。纯虚函数声明如下:

virtual void funtion1() = 0; 

纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。

包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。


上面那些概念大家可以捋一捋,毕竟C++概念还是挺多的。为什么说到多态要与虚函数和纯虚函数扯上关系?光说概念没有实例确实难理解。下面我们还是以我们熟悉的狗类和动物类,另外加一个猫类进行多态的讲解。

新建一个目录09_polymorphism_example。

#include <iostream>
#include <string>
using namespace std;

/* 定义一个动物类*/
class Animal
{
	public:
		virtual void run()	// 虚函数
		{
			cout << "Animal的run()方法" << endl;
		}
};

/* 定义一个狗类,并继承动物类*/
class Dog : public Animal
{
	public:
		void run()
		{
			cout << "Dog的run()方法" << endl;
		}
};

/* 定义一个猫类,并继承动物类*/
class Cat : public Animal
{
	public:
		void run()
		{
			cout << "Cat的run()方法" << endl;
		}
};

int main()
{
	/* 声明一个Animal的指针对象,注:并没有实例化*/
	Animal *animal;
	/* 实例化dog对象*/
	Dog dog;
	/* 实例化cat对象*/
	Cat cat;

	/* 存储dog对象的地址*/
	animal = &dog;
	/* 调用run()方法*/
	animal->run();

	/* 存储cat对象的地址*/
	animal = &cat;
	/* 调用run()方法*/
	animal->run();
	return(0);
}

第9 行、第18 行和第28 行,都有一个run()方法。其中我们可以看到基类Animal 类的run()方法前面加了关键字 virtual。这样让基类Animal 类的run()方法变成了虚函数。在这个例子里我们可以知道虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。简单的来说,上面的实例是基类Animal 声明了一个指针animal。然后通过基类的指针来访问 Dog 类对象与 Cat 类对象的 run() 方法,前提是基类的run()方法必须声明为虚函数,如果不声明为虚函数,基类的指针将访问到基类自己的run()方法。我们可以尝试把virtual 关键字去掉再重新编译测试,如果不加关键字virtual 会是什么情况。

第44 行和第49 行,可以理解是animal 指针实例化的过程。当基类的run()方法定义成虚函数,编译器不静态链接到该函数,它将链接到派生类的run()方法,进行实例化。
执行下面的指令编译。

g++ 09_polymorphism_example.cpp -o 09_polymorphism_example

编译完成执行的结果如下。
在这里插入图片描述

数据封装

封装是面向对象编程中的把数据和操作数据的函数绑定在一起的概念,这样能避免受到外界的干扰和误用,从而确保了安全。数据封装引申出了另一个重要的OOP 概念,即数据隐藏。

数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制,C++ 通过创建类来支持封装和数据隐藏(public、protected、private)。

其实我们在第2.2 小节开始就已经接触了数据封装。在C++程序中,任何带有公有和私有成员的类都可以作为数据封装和数据抽象的实例。通常情况下,我们都会设置类成员状态为私有(private),除非我们真的需要将其暴露,这样才能保证良好的封装性。这通常应用于数据成员,但它同样适用于所有成员,包括虚函数。

下面我们还是以狗类为例,增加一个食物的方法addFood(int number)。将获得食物的方法设定在public 下,这样addFood(int number)方法就暴露出来了,也就是对外的接口。然后我们设置狗类的私有成员(private)食物的份数total。我们在这个教程里第一次使用private,在这章节里我们也可以学到什么时候该使用private 什么时候使用public。total 为获得的食物总数,然后我们还写一个公开的方法getFood()在public 下,通过getFood()来打印出小狗总共获得了几份食物。

新建一个目录10_encapsulation_example,编辑一个10_encapsulation_example.cpp 内容如下。

#include <iostream>
#include <string>
using namespace std;

class Dog
{
	public:
		string name;
	
		Dog( int i = 0 )
		{
			total = i;
		}
	
	
		void addFood( int number )
		{
			total = total + number;
		}
	
	
		int getFood()
		{
			return(total);
		}
	
	
	private:
		int total;
};


int main()
{
	Dog dog;

	dog.name = "旺财";

	dog.addFood( 3 );
	dog.addFood( 2 );

	cout << dog.name << "总共获得了" << dog.getFood() << "份食物" << endl;

	return(0);
}

第10 至第13 行,在构造函数里初始化total 的数量,不初始化total 的数量默认是随int 类型的数。所以我们需要在构造函数里初始化,也体现了构造函数的功能,一般是在构造函数里初始化。不要在类内直接赋值初始化,有可能有些编译器不支持。
第15 至17 行,addFood(int number),在这个方法里,将获得的食物份数赋值给total。
第19 至21,getFood(),在这个方法里,将返回食物的总份数。通过调用这个方法,即可访问私有成员的total 总数。
第33 和34 行,添加食物的份数。
第36 行,打印食物的总份数。

执行下面的指令编译。

g++ 10_encapsulation_example.cpp -o 10_encapsulation_example

编译完成执行的结果如下。

在这里插入图片描述

数据抽象

数据抽象是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。数据抽象是一种依赖于接口和实现分离的编程(设计)技术。
数据抽象的好处:

  1. 类的内部受到保护,不会因无意的用户级错误导致对象状态受损。
  2. 类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要
    求不改变用户级代码的错误报告。

就C++ 编程而言,C++ 类为数据抽象提供了可能。它们向外界提供了大量用于操作对象数据的公共方法,也就是说,外界实际上并不清楚类的内部实现。

其实像cout 这个对象就是一个公共的接口,我们不必要知道cout 是如何在屏幕上显示内容的。cout 已经在底层实现好了。

在上一节我们已经学习过数据封装,数据封装是一种把数据和操作数据的函数捆绑在一起的机制,而数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。

C++ 程序中,任何带有公有和私有成员的类都可以作为数据抽象的实例。例子略可参考上2.2.5 小节的例子。

接口(抽象类,使用纯虚函数)

接口描述了类的行为和功能,而不需要完成类的特定实现。C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用"= 0" 来指定的。

设计抽象类(通常称为ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。
抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。

因此,如果一个ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着C++ 支持使用ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。可用于实例化对象的类被称为具体类。


根据概念我们来写个实例来说明抽象类。

还是以狗类为说明,例程与2.2.4 小节类似,只是Aninmal 类的run()方法定义为纯虚函数,纯虚函数不用实现,由派生类Dog 和Cat 类实现重写即可。

新建一个目录11_abstract_class,编辑一个11_abstract_class.cpp 内容如下。

#include <iostream>

using namespace std;

/* 定义一个动物类*/
class Animal
{
	public:
		virtual void run() = 0;  // =0 是纯虚函数
};

/* 定义一个狗类,并继承动物类*/
class Dog : public Animal
{
	public:
		void run()
		{
			cout << "Dog的run()方法" << endl;
		}
};

/* 定义一个猫类,并继承动物类*/
class Cat : public Animal
{
	public:
		void run()
		{
			cout << "Cat的run()方法" << endl;
		}
};

int main()
{
	/* 实例化dog对象*/
	Dog dog;

	/* 实例化cat对象*/
	Cat cat;

	/* dog调用run()方法*/
	dog.run();

	/* cat调用run()方法*/
	cat.run();

	return(0);
}

执行下面指令进行程序编译。

g++ 11_abstract_class.cpp -o 11_abstract_class

程序运行的结果如下。

在这里插入图片描述

虽然结果和例程与2.2.4 小节一样,但是却表现了两种不同的思想。学C++重要的是思想,当我们对这种思想有一种的了解后,不管是Qt 或者其他C++程序,我们都能快速学习和了解。

C++的内容就到此结束了。在这个C++基础中,我们的例子非常简单,也十分之易懂,重要的是理解概念,许多C++的课程都是以C++的功能甚至是很复杂的算法作讲解,内容复杂且多。

只要我们理解好上面的C++的基础,对学习C++有很大的帮助,不要求对C++有很深的理解,至少在我们后面学习Qt 时已经大概了解Qt 中的C++语法。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行稳方能走远

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值