1.C++简介
C++ (c plus plus) 是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,支持过程化编程、面向对象编程和泛型编程。 C++ 被认为是一种中级语言,它综合了高级语言和低级语言的特点。 C++ 是由 Bjarne Stroustrup 于 1979 年在新泽西州美利山贝尔实验室开始设计开发的。 C++ 进一步扩充和完善了 C 语言,最初命名为带类的 C,后来在 1983 年更名为 C++。C++ 是 C 的一个超集,事实上,任何合法的 C 程序都是合法的 C++ 程序。截止 2020 年,在 2017 年发布 C++17,已经是第五个 C++标准了。我们也见过或者听过 C++98,这样的 C++标准,也就是 1998 年发布的 C++,所以叫 C++98,是 C++的第一个标准。
学习C++我们要理解概念,而不是深究语言技术细节。 我们只要带着第二章的 C++基础概念,学习 Qt 或者写 C++会有一定的帮助。
1.1c++新特性
C++比 C 语言新增的数据类型是布尔类型(bool)。 但是在新的 C 语言标准里已经有布尔类型了,但是在旧的C语言标准里是没有布尔类型的,编译器也无法解释布尔类型。
在传统的C 语言里,变量初始化时必须在程序的前面定义在前面, 而 C++则是可以随用随定义。 C++也可以直接初始化,比如 int x(100);这样就直接赋值 x=100,这些都是 C++特性的好处。 这里只说这些常用的新特性,其他特性不做描述或者解释了。
1.2c++的输入输出方式
在 C 语言里, 我们是这样输入或者输出的。
在 C++里,我们使用以 cin 和 cout 代替了 scanf 和 printf。在输入和输出的流程上是不变的,只是关键字变了,用法也变了。
要说效率上,肯定是 C 语言的 scanf 和 printf 的效率高, 但是没有 C++中的 cin 和 cout 使用方便。
C++的 I/O 语法方式如下。
cout 语法形式:
1 cout<<x<<endl;x 可以是任意数据类型,甚至可以写成一个表达式, 这比 C 语言需要指定数据类型方便多了, endl 指的是换行符,与 C 语言的“\n” 效果一样。
1.3c++之命名空间 namespace
注意第 1 行,不能写成 iostream.h, 有.h 的是非标准的输入输出流, c 的标准库。无.h 的是标准输入输出流就要用命名空间。
1
2
3
4
5
6
7
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。
2.c++面向对象
面向对象的三大特征是继承,多态和封装
2.1类和对象
C++ 在 C 语言的基础上增加了面向对象编程, C++ 支持面向对象程序设计。
类用于指定对象的形式,它包含了数据表示法和用于处理数据的方法。类中的数据和方法称为类的成员。函数在一个类中被称为类的成员。
从类中实例化对象分两种方法,一种是从栈中实例化对象,一种是从堆中实例化对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using namespace std;
class Dog
{
public:
//访问限定符 public(公有的), 此外还有 private(私有的) 和 protected(受保护的)。写这个的目的是为了下面我们要调用这些成员,不写访问限定符默认是 private。
string name;
int age;
void run() {
cout<<"小狗的名字是:"<<name<<","<<"年龄是"<<age<<endl;
}
};
int main()
{
Dog dog1; //从栈中实例化一个对象 dog1(可以随意起名字)。
dog1.name = "旺财";
dog1.age = 2;
dog1.run();
Dog *dog2 = new Dog(); //从堆中实例化对象,使用关键字 new 的都是从堆中实例化对象。
dog2->name = "富贵";
dog2->age = 1;
dog2->run();
delete dog2; //释放内存,将 dog2 重新指向 NULL。
dog2 = NULL;
return 0;
}通过上面的例子我们已经学习了什么是类,和什么是对象。 以描述 Dog 为一类(抽象出来的),从 Dog 类中实例出来就是对象(实际事物)。 对象拥有 Dog 类里的属性,可以从栈中实例化对象,亦可从堆中实例化对象。 类的编写过程和对象的使用过程大致如上了。我们只需要理解这个步骤,明白类的定义和使用即可。
2.1.1构造函数和析构函数
什么是构造函数?构造函数在对象实例化时被系统自动调用,仅且调用一次。 构造函数出现在哪里?前面我们学过类, 实际上定义类时, 如果没有定义构造函数和析构函数, 编译器就会生成一个构造函数和析构函数, 只是这个构造和析构函数什么事情也不做,所以我们不会注意到一点。
构造函数的特点如下:
(1) 构造函数必须与类名同名;
(2) 可以重载;
(3) 没有返回类型,即使是 void 也不行。
什么是析构函数?与构造函数相反, 在对象结束其生命周期时系统自动执行析构函数。 实际上定义类时,编译器会生成一个析构函数。
析构函数的特点如下:
(1) 析构函数的格式为 ~类名();
(2) 调用时释放内存(资源);
(3) ~类名()不能加参数;
(4) 没有返回值,即使是 void 也不行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using namespace std;
//定义了一个狗类,并在里面写了构造函数和析构函数。
class Dog
{
public:
Dog();
~Dog();
};
int main()
{
Dog dog;
cout<<"构造与析构函数示例"<<endl;
return 0;
}
//类的函数可以在类里实现,也可以在类外实现,不过在类外实现时需要使用“::”,此时我们把类的构造函数定义在类的外面, 打印一句"构造函数执行! "。
Dog::Dog()
{
cout<<"构造函数执行! "<<endl;
}
//类的析造函数定义在类的外面, 打印一句"析造函数执行! "。
Dog::~Dog()
{
cout<<"析构函数执行! "<<endl;
}
在对象实例化时会调用构造函数,所以 Dog()先执行,然后再在 main()函数里继续执行 cout<<”构造与析构函数示例”<<endl;。最后对象生命周期结束时才会执行析构函数。
2.1.2this指针
一个类中的不同对象在调用自己的成员函数时,其实它们调用的是同一段函数代码,那么成员函数如何知道要访问哪个对象的数据成员呢?
没错,就是通过 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 指针会因编译器不同而有不同的放置位置。可能是栈,也可能是寄存器,甚至全
局变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using namespace std;
class Dog
{
public:
string name;
void func();
};
int main()
{
Dog dog;
dog.func();
return 0;
}
void Dog::func()
{
this->name = "旺财";
//在类的成员函数里使用了 this 指针, 并指向了类里的成员 name。 先将 name赋值叫“旺财”,然后我们打印 name 的值。
cout<<"小狗的名字叫: "<<this->name<<endl;
}
2.2继承
面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。
一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:
1 class derived-class: access-specifier base-class与类的访问修饰限定符一样,继承的方式也有几种。 其中,访问修饰符 access-specifier 是public、 protected 或 private 其中的一个, base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private。
继承的方式 :
- 公有继承(public):当一个类派生继承公有基类时,基类的公有成员也是派生类的公有成
员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但
是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生继承保护基类时,基类的公有和保护成员将成为派
生类的保护成员。
- 私有继承(private):当一个类派生继承私有基类时,基类的公有和保护成员将成为派生类
的私有成员。
这个公有成员和继承方式也没有什么特别的,无非就是不同的访问权限而已, 可以这样简单的理解。
2.3重载
C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。
当您调用一个重载函数或重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策。
2.3.1函数重载
在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。
下面通过一个小实例来简单说明一下函数重载的用法。 我们还是以狗类为说明, 现在假设有个需求。 我们需要打印狗的体重, 分别以整数记录旺财的体重和小数记录旺财的体重, 同时以整数打印和小数打印旺财的体重。 那么我们可以通过函数重载的方法实现这个简单的功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using namespace std;
class Dog
{
public:
string name;
//写了一个方法 getWeight(int weight),以 int 类型作为参数。
void getWeight(int weight) {
cout<<name<<"的体重是: "<<weight<<"kG"<<endl;
}
//以相同的函数名 getWeight,不同的参数类型 double weight,这样就构成了函数重载。
void getWeight(double weight) {
cout<<name<<"的体重是: "<<weight<<"kG"<<endl;
}
};
int main()
{
Dog dog;
dog.name = "旺财";
//分别传进参数不同的参数,程序就会匹配不同的重载函数
dog.getWeight(10);
dog.getWeight(10.5);
return 0;
}
2.3.2运算符重载
运算符重载的实质就是函数重载或函数多态。运算符重载是一种形式的 C++多态。目的在于让人能够用同名的函数来完成不同的基本操作。要重载运算符,需要使用被称为运算符函数的特殊函数形式,运算符函数形式:
operatorp(argument-list)
, operator 后面的’p’为要重载的运算符符号。 重载运算符的格式如下:
1
2
3
4 <返回类型说明符> operator <运算符符号>(<参数表>)
{
<函数体>
}
下面是可重载的运算符列表:
下面是不可重载的运算符列表:
成员访问运算符 . 成员指针访问运算符 ., -> 域运算符 :: 长度运算符 sizeof 条件运算符 ?: 预处理符号 # 根据上表我们知道可以重载的运算符有很多,我们以重载“+”运算符为例,实际上用重载运算符我们在实际应用上用的比较少,我们只需要了解和学习这种思想即可。
下面的实例使用成员函数演示了运算符重载的概念。在这里,对象作为参数进行传递,对象的属性使用 this 运算符进行访问。 下面还是以我们熟悉的狗类为例。 声明加法运算符用于把两个Dog 对象相加的体重相加,返回最终的 Dog 对象然后得到第三个 Dog 对象的体重。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using namespace std;
class Dog
{
public:
int weight;
//重载“+”运算符,注意函数必须与类名同名, 把 Dog 对象作为传递, 使用this 运算符进行访问。然后返回一个 dog 对象。
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;
}
2.4多态
C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数;形成多态必须具备三个条件:
必须存在继承关系;
继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字 virtual 声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数);
存在基类类型的指针或者引用,通过该指针或引用调用虚函数。
虚函数:
是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。 虚函数声明如下:
virtual ReturnType FunctionName(Parameter)
虚函数必须实现,如果不实现,编译器将报错纯虚函数:
若在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。 纯虚函数声明如下:
virtual void funtion1()=0;
纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
using namespace std;
/* 定义一个动物类 */
class Animal
{
public:
//看到基类 Animal 类的 run()方法前面加了关键字 virtual。 这样让基类 Animal 类的 run()方法变成了虚函数。
//虚函数是 C++中用于实现多态(polymorphism)的机制。 核心理念就是通过基类访问派生类定义的函数。
/*简单的来说,上面的实例是基类 Animal 声明了一个指针 animal。然后通过基类的指针来访问 Dog 类对象与 Cat 类的对象的 run()方法,前提是基类的 run()方法必须声明为虚函数,如果不声明为虚函数,基类的指针将访问到基类自己的 run()方法。我们可以尝试把 virtual 关键字去掉再重新编译测试,如果不加关键字 virtual 会是什么情况。*/
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;
}
2.5数据封装
封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。数据封装引申出了另一个重要的 OOP 概念,即数据隐藏。
数据封装是一种把数据和操作数据的函数捆绑在一起的机制, 数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制,C++ 通过创建类来支持封装和数据隐藏(public、protected、 private)。
在 C++程序中,任何带有公有和私有成员的类都可以作为数据封装和数据抽象的实例。 通常情况下,我们都会设置类成员状态为私有(private),除非我们真的需要将其暴露,这样才能保证良好的封装性。这通常应用于数据成员,但它同样适用于所有成员,包括虚函数。
下面我们还是以狗类为例, 增加一个食物的方法
addFood(int number)
。将获得食物的方法设定在 public 下,这样 addFood(int number)方法就暴露出来了,也就是对外的接口。然后我们设置狗类的私有成员(private)食物的份数 total。 我们在这个教程里第一次使用 private, 在这章节里我们也可以学到什么时候该使用 private 什么时候使用 public。 total 为获得的食物总数,然后我们还写一个公开的方法 getFood()在 public 下,通过 getFood()来打印出小狗总共获得了几份食物。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using namespace std;
class Dog
{
public:
string name;
/*在构造函数里初始化 total 的数量,不初始化 total 的数量默认是随 int 类型的数。所以我们需要在构造函数里初始化, 也体现了构造函数的功能,一般是在构造函数里初始化。 不要在类内直接赋值初始化,有可能有些编译器不支持。*/
Dog(int i = 0)
{
total = i;
}
/*addFood(int number),在这个方法里,将获得的食物份数赋值给 total。*/
void addFood(int number) {
total = total + number;
}
/*getFood(),在这个方法里,将返回食物的总份数。 通过调用这个方法,即可
访问私有成员的 total 总数。*/
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;
}这个例子告诉了如何得到私有数据。
2.6数据抽象
数据抽象是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。数据抽象是一种依赖于接口和实现分离的编程(设计)技术。
数据抽象的好处:
类的内部受到保护,不会因无意的用户级错误导致对象状态受损。
类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告。
举个简单的例子,比如我们生活中的手机。 手机可以拍照、听音乐、 收音等等。 这些都是手机上的功能,用户可以直接使用。 但是拍照的功能是如何实现的,是怎么通过摄像头取像然后怎么在屏幕上显示的过程,作为用户是不需要知道的。 也就是暴露的不用太彻底,用户也不必须知道这种功能是如何实现的,只需要知道如何拍照即可。
就C++ 编程而言, C++ 类为数据抽象提供了可能。它们向外界提供了大量用于操作对象数据的公共方法,也就是说,外界实际上并不清楚类的内部实现。
其实像cout 这个对象就是一个公共的接口,我们不必要知道 cout 是如何在屏幕上显示内容的。 cout 已经在底层实现好了。
在上一节我们已经学习过数据封装, 数据封装是一种把数据和操作数据的函数捆绑在一起的机制, 而数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
C++ 程序中,任何带有公有和私有成员的类都可以作为数据抽象的实例。
2.7接口(抽象类)
接口描述了类的行为和功能,而不需要完成类的特定实现。 C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。 如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的。
设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。
因此,如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。可用于实例化对象的类被称为具体类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 #include <iostream>
using namespace std;
/* 定义一个动物类 */
class Animal
{
public:
virtual void run() = 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;
}
虽然结果和例程与 2.4 小节一样,但是却表现了两种不同的思想。学 C++重要的是思想,当我们对这种思想有一种的了解后, 不管是 Qt 或者其他 C++程序,我们都能快速学习和了解。