《C++面向对象程序设计》✍千处细节、万字总结
文章目录
一、面向对象程序设计
面向对象程序设计(Object-Oriented Programming
,OOP
)是一种新的程序设计范型。程序设计范型是指设计程序的规范、模型和风格,它是一类程序设计语言的基础。
面向过程程序设计范型是使用较广泛的面向过程性语言,其主要特征是:程序由过程定义和过程调用组成(简单地说,过程就是程序执行某项操作的一段代码,函数就是最常用的过程)。
面向对象程序的基本元素是对象,面向对象程序的主要结构特点是:第一,程序一般由类的定义和类的使用两部分组成;第二,程序中的一切操作都是通过向对象发送消息来实现的,对象接收到消息后,启动有关方法完成相应的操作。
对象:描述其属性的数据以及对这些数据施加的一组操作封装在一起构成的统一体。对象可认为是数据+操作。
类:类是具有相同的数据和相同的操作的一组对象的集合。
消息传递:对象之间的交互。
**方法:**对象实现的行为称为方法。
面向对象程序设计的基本特征:抽象、封装、继承、多态。
二、C++基础
~
2.1 C++的产生和特点
C++是美国贝尔实验室的Bjarne Stroustrup博士在C语言的基础上,弥补了C语言存在的一些缺陷,增加了面向对象的特征,于1980年开发出来的一种面向过程性与面向对象性相结合的程序设计语言。最初他把这种新的语言称为“含类的C”,到1983年才取名为C++。
相比C语言,C++的主要特点是增加了面向对象机制。
~
2.2 一个简单的C++示例程序
详细创建步骤可参考博客【Visual Studio】 创建C/C++项目
#include <iostream> //编译预处理命令
using namespace std; //使用命名空间
int add(int a, int b); //函数原型说明
int main() //主函数
{
int x, y;
cout << "Enter two numbers: " << endl;
cin >> x;
cin >> y;
int sum = add(x, y);
cout << "The sum is : " << sum << ‘\n’;
return 0;
}
int add(int a, int b) //定义add()函数,函数值为整型
{
return a + b;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
~
2.3 C++在非面向对象方面对C语言的扩充
输入和输出
int i;
float f;
cin >> i;
cout << f;
------------
scanf("%d", &i);
printf("%f", f);
----------------
连续读入
cin >> a >> b >> c;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
cin
- 在默认情况下,运算符“
>>
”将跳过空白符,然后读入后面与变量类型相对应的值。因此,给一组变量输入值时可用空格符、回车符、制表符将输入的数据间隔开。 - 当输入字符串(即类型为string的变量)时,提取运算符“
>>
”的作用是跳过空白字符,读入后面的非空白字符,直到遇到另一个空白字符为止,并在串尾放一个字符串结束标志‘\0
’。
~
C++允许在代码块中的任何地方声明局部变量。
const修饰符
在C语言中,习惯使用#define
来定义常量,例如#define PI 3.14
,C++提供了一种更灵活、更安全的方式来定义常量,即使用const
修饰符来定义常量。例如const float PI = 3.14;
const可以与指针一起使用,它们的组合情况复杂,可归纳为3种:指向常量的指针、常指针和指向常量的常指针。
-
指向常量的指针:一个指向常量的指针变量。
const char* pc = "abcd"; 该方法不允许改变指针所指的变量,即 pc[3] = ‘x'; 是错误的, 但是,由于pc是一个指向常量的普通指针变量,不是常指针,因此可以改变pc所指的地址,例如 pc = "ervfs"; 该语句付给了指针另一个字符串的地址,改变了pc的值。
- 1
- 2
- 3
- 4
- 5
- 6
-
常指针:将指针变量所指的地址声明为常量
char* const pc = "abcd"; 创建一个常指针,一个不能移动的固定指针,可更改内容,如 pc[3] = 'x'; 但不能改变地址,如 pc = 'dsff'; 不合法
- 1
- 2
- 3
- 4
- 5
-
指向常量的常指针:这个指针所指的地址不能改变,它所指向的地址中的内容也不能改变。
const char* const pc = "abcd"; 内容和地址均不能改变
- 1
- 2
说明:
如果用const定义整型常量,关键字可以省略。即
const in bufsize = 100
与const bufsize = 100
等价;常量一旦被建立,在程序的任何地方都不能再更改。
与#define不同,const定义的常量可以有自己的数据类型。
函数参数也可以用const说明,用于保证实参在该函数内不被改动。
void型指针
void通常表示无值,但将void作为指针的类型时,它却表示不确定的类型。这种void型指针是一种通用型指针,也就是说任何类型的指针值都可以赋给void类型的指针变量。
需要指出的是,这里说void型指针是通用指针,是指它可以接受任何类型的指针的赋值,但对已获值的void型指针,对它进行再处理,如输出或者传递指针值时,则必须再进行显式类型转换,否则会出错。
void* pc;
int i = 123;
char c = 'a';
pc = &i;
cout << pc << endl; //输出指针地址006FF730
cout << *(int*)pc << endl; //输出值123
pc = &c;
cout << *(char*)pc << endl; //输出值a
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
内联函数
在函数名前冠以关键字inline
,该函数就被声明为内联函数。每当程序中出现对该函数的调用时,C++编译器使用函数体中的代码插入到调用该函数的语句之处,同时使用实参代替形参,以便在程序运行时不再进行函数调用。引入内联函数主要是为了消除调用函数时的系统开销,以提高运行速度。
说明:
- 内联函数在第一次被调用之前必须进行完整的定义,否则编译器将无法知道应该插入什么代码
- 在内联函数体内一般不能含有复杂的控制语句,如for语句和switch语句等
- 使用内联函数是一种空间换时间的措施,若内联函数较长,较复杂且调用较为频繁时不建议使用
#include <iostream>
using namespace std;
inline double circle(double r) //内联函数
{
double PI = 3.14;
return PI r r;
}
int main()
{
for (int i = 1; i <= 3; i++)
cout << "r = “ << i << ” area = " << circle(i) << endl;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
使用内联函数替代宏定义,能消除宏定义的不安全性
带有默认参数值的函数
当进行函数调用时,编译器按从左到右的顺序将实参与形参结合,若未指定足够的实参,则编译器按顺序用函数原型中的默认值来补足所缺少的实参。
void init(int x = 5, int y = 10);
init (100, 19); // 100 , 19
init(25); // 25, 10
init(); // 5, 10
- 1
- 2
- 3
- 4
-
在函数原型中,所有取默认值的参数都必须出现在不取默认值的参数的右边。
如 int fun(int a, int b, int c = 111);
- 1
-
在函数调用时,若某个参数省略,则其后的参数皆应省略而采取默认值。不允许某个参数省略后,再给其后的参数指定参数值。
函数重载
在C++中,用户可以重载函数。这意味着,在同一作用域内,只要函数参数的类型不同,或者参数的个数不同,或者二者兼而有之,两个或者两个以上的函数可以使用相同的函数名。
#include <iostream>
using namespace std;
int add(int x, int y)
{
return x + y;
}
double add(double x, double y)
{
return x + y;
}
int add(int x, int y, int z)
{
return x + y + z;
}
int main()
{
int a = 3, b = 5, c = 7;
double x = 10.334, y = 8.9003;
cout << add(a, b) << endl;
cout << add(x, y) << endl;
cout << add(a, b, c) << endl;
return 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
说明:
-
调用重载函数时,函数返回值类型不在参数匹配检查之列。因此,若两个函数的参数个数和类型都相同,而只有返回值类型不同,则不允许重载。
int mul(int x, int y); double mul(int x, int y);
- 1
- 2
-
函数的重载与带默认值的函数一起使用时,有可能引起二义性。
void Drawcircle(int r = 0, int x = 0, int y = 0); void Drawcircle(int r); Drawcircle(20);
- 1
- 2
- 3
-
在调用函数时,如果给出的实参和形参类型不相符,C++的编译器会自动地做类型转换工作。如果转换成功,则程序继续执行,在这种情况下,有可能产生不可识别的错误。
void f_a(int x); void f_a(long x); f_a(20.83);
- 1
- 2
- 3
作用域标识符"::"
通常情况下,如果有两个同名变量,一个是全局的,另一个是局部的,那么局部变量在其作用域内具有较高的优先权,它将屏蔽全局变量。
如果希望在局部变量的作用域内使用同名的全局变量,可以在该变量前加上“::
”,此时::value
代表全局变量value,“::
”称为作用域标识符。
#include <iostream>
using namespace std;
int value; //定义全局变量value
int main()
{
int value; //定义局部变量value
value = 100;
::value = 1000;
cout << "local value : " << value << endl;
cout << "global value : " << ::value << endl;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
强制类型转换
可用强制类型转换将不同类型的数据进行转换。例如,要把一个整型数(int)转换为双精度型数(double),可使用如下的格式:
int i = 10;
double x = (double)i;
或
int i = 10;
double x = double(i);
- 1
- 2
- 3
- 4
- 5
以上两种方法C++都能接受,建议使用后一种方法。
new和delete运算符
程序运行时,计算机的内存被分为4个区:程序代码区、全局数据区、堆和栈。其中,堆可由用户分配和释放。C语言中使用函数malloc()
和free()
来进行动态内存管理。C++则提供了运算符new
和delete
来做同样的工作,而且后者比前者性能更优越,使用更灵活方便。
指针变量名 = new 类型
int *p;
p = new int;
delete 指针变量名
delete p;
- 1
- 2
- 3
- 4
- 5
下面对new和delete的使用再做一下几点说明:
-
用运算符new分配的空间,使用结束后应该用也只能用delete显式地释放,否则这部分空间将不能回收而变成死空间。
-
在使用运算符new动态分配内存时,如果没有足够的内存满足分配要求,new将返回空指针(
NULL
)。 -
使用运算符new可以为数组动态分配内存空间,这时需要在类型后面加上数组大小。
指针变量名 = new 类型名[下标表达式]; int *p = new int[10];
- 1
- 2
释放动态分配的数组存储区时,可使用delete运算符。
delete []指针变量名; delete p;
- 1
- 2
-
new 可在为简单变量分配空间的同时,进行初始化
指针变量名 = new 类型名(初值); int *p; p = new int(99); ··· delete p;
- 1
- 2
- 3
- 4
- 5
引用
引用(reference
)是C++对C的一个重要扩充。变量的引用就是变量的别名,因此引用又称别名。
类型 &引用名 = 已定义的变量名
- 1
引用与其所代表的变量共享同一内存单元,系统并不为引用另外分配存储空间。实际上,编译系统使引用和其代表的变量具有相同的地址。
#include <iostream>
using namespace std;
int main()
{
int i = 10;
int &j = i;
cout << "i = " << i << " j = " << j << endl;
cout << "i的地址为 " << &i << endl;
cout << "j的地址为 " << &j << endl;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
上面代码输出i和j的值相同,地址也相同。
- 引用并不是一种独立的数据类型,它必须与某一种类型的变量相联系。在声明引用时,必须立即对它进行初始化,不能声明完成后再赋值。
- 为引用提供的初始值,可以是一个变量或者另一个引用。
- 指针是通过地址间接访问某个变量,而引用则是通过别名直接访问某个变量。
引用作为函数参数、使用引用返回函数值
#include <iostream>
using namespace std;
void swap(int &a, int &b)
{
int t = a;
a = b;
b = t;
}
int a[] = { 1, 3, 5, 7, 9};
int& index(int i)
{
return a[i];
}
int main()
{
int a = 5, b = 10;
//交换数字a和b
swap(a, b);
cout << "a = “ << a << ” b = " << b << endl;
cout << index(2) << endl; //等价于输出元素a[2]的值
index(2) = 100; //等价于将a[2]的值赋为100;
cout << index(2) << endl;
<span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
}
- 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
对引用的进一步说明
- 不允许建立void类型的引用
- 不能建立引用的数组
- 不能建立引用的引用。不能建立指向引用的指针。引用本身不是一种数据类型,所以没有引用的引用,也没有引用的指针。
- 可以将引用的地址赋值给一个指针,此时指针指向的是原来的变量。
- 可以用const对引用加以限定,不允许改变该引用的值,但是它不阻止引用所代表的变量的值。
三、类和对象(一)
~
3.1 类的构成
类声明中的内容包括数据和函数,分别称为数据成员和成员函数。按访问权限划分,数据成员和成员函数又可分为共有、保护和私有3种。
class 类名{
public:
公有数据成员;
公有成员函数;
protected:
保护数据成员;
保护成员函数;
private:
私有数据成员;
私有成员函数;
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
如成绩类
class Score{
public:
void setScore(int m, int f);
void showScore();
private:
int mid_exam;
int fin_exam;
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 对一个具体的类来讲,类声明格式中的3个部分并非一定要全有,但至少要有其中的一个部分。一般情况下,一个类的数据成员应该声明为私有成员,成员函数声明为共有成员。这样,内部的数据整个隐蔽在类中,在类的外部根本就无法看到,使数据得到有效的保护,也不会对该类以外的其余部分造成影响,程序之间的相互作用就被降低到最小。
- 类声明中的关键字private、protected、public可以任意顺序出现。
- 若私有部分处于类的第一部分时,关键字private可以省略。这样,如果一个类体中没有一个访问权限关键字,则其中的数据成员和成员函数都默认为私有的。
- 不能在类声明中给数据成员赋初值。
~
3.2 成员函数的定义
普通成员函数的定义
在类的声明中只给出成员函数的原型,而成员函数的定义写在类的外部。这种成员函数在类外定义的一般形式是:
返回值类型 类名::成员函数名(参数表){ 函数体}
- 1
例如,表示分数的类Score可声明如下:
class Score{
public:
void setScore(int m, int f);
void showScore();
private:
int mid_exam;
int fin_exam;
};
void Score::setScore(int m, int f)
{
mid_exam = m;
fin_exam = f;
}
void Score::showScore()
{
cout << "期中成绩: " << mid_exam << endl;
cout << “期末成绩:” << fin_exam << endl;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
内联成员函数的定义
- 隐式声明:将成员函数直接定义在类的内部
class Score{
public:
void setScore(int m, int f)
{
mid_exam = m;
fin_exam = f;
}
void showScore()
{
cout << "期中成绩: " << mid_exam << endl;
cout << "期末成绩:" << fin_exam << endl;
}
private:
int mid_exam;
int fin_exam;
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 显式声明:在类声明中只给出成员函数的原型,而将成员函数的定义放在类的外部。
class Score{
public:
inline void setScore(int m, int f);
inline void showScore();
private:
int mid_exam;
int fin_exam;
};
inline void Score::setScore(int m, int f)
{
mid_exam = m;
fin_exam = f;
}
inline void Score::showScore()
{
cout << "期中成绩: " << mid_exam << endl;
cout << “期末成绩:” << fin_exam << endl;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
说明:在类中,使用inline定义内联函数时,必须将类的声明和内联成员函数的定义都放在同一个文件(或同一个头文件)中,否则编译时无法进行代码置换。
~
3.3 对象的定义和使用
通常把具有共同属性和行为的事物所构成的集合称为类。
类的对象可以看成该类类型的一个实例,定义一个对象和定义一个一般变量相似。
对象的定义
- 在声明类的同时,直接定义对象
class Score{
public:
void setScore(int m, int f);
void showScore();
private:
int mid_exam;
int fin_exam;
}op1, op2;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 声明了类之后,在使用时再定义对象
Score op1, op2;
- 1
对象中成员的访问
对象名.数据成员名对象名.成员函数名[(参数表)]op1.setScore(89, 99);
op1.showScore();
- 1
- 2
说明:
-
在类的内部所有成员之间都可以通过成员函数直接访问,但是类的外部不能访问对象的私有成员。
-
在定义对象时,若定义的是指向此对象的指针变量,则访问此对象的成员时,不能用“
.
”操作符,而应该使用“->
“操作符。如Score op, *sc; sc = &op; sc->setScore(99, 100); op.showScore();
- 1
- 2
- 3
- 4
类的作用域和类成员的访问属性
私有成员只能被类中的成员函数访问,不能在类的外部,通过类的对象进行访问。
一般来说,公有成员是类的对外接口,而私有成员是类的内部数据和内部实现,不希望外界访问。将类的成员划分为不同的访问级别有两个好处:一是信息隐蔽,即实现封装,将类的内部数据与内部实现和外部接口分开,这样使该类的外部程序不需要了解类的详细实现;二是数据保护,即将类的重要信息保护起来,以免其他程序进行不恰当的修改。
对象赋值语句
Score op1, op2;
op1.setScore(99, 100);
op2 = op1;
op2.showScore();
- 1
- 2
- 3
- 4
~
3.4 构造函数与析构函数
构造函数
构造函数是一种特殊的成员函数,它主要用于为对象分配空间,进行初始化。构造函数的名字必须与类名相同,而不能由用户任意命名。它可以有任意类型的参数,但不能具有返回值。它不需要用户来调用,而是在建立对象时自动执行。
class Score{
public:
Score(int m, int f); //构造函数
void setScore(int m, int f);
void showScore();
private:
int mid_exam;
int fin_exam;
};
Score::Score(int m, int f)
{
mid_exam = m;
fin_exam = f;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
在建立对象的同时,采用构造函数给数据成员赋值,通常由以下两种形式
-
类名 对象名[(实参表)] Score op1(99, 100); op1.showScore();
- 1
- 2
- 3
-
类名 *指针变量名 = new 类名[(实参表)] Score *p; p = new Score(99, 100); p->showScore(); ----------------------- Score *p = new Score(99, 100); p->showScore();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
说明:
- 构造函数的名字必须与类名相同,否则编译程序将把它当做一般的成员函数来处理。
- 构造函数没有返回值,在定义构造函数时,是不能说明它的类型的。
- 与普通的成员函数一样,构造函数的函数体可以写在类体内,也可写在类体外。
- 构造函数一般声明为共有成员,但它不需要也不能像其他成员函数那样被显式地调用,它是在定义对象的同时被自动调用,而且只执行一次。
- 构造函数可以不带参数。
成员初始化列表
在声明类时,对数据成员的初始化工作一般在构造函数中用赋值语句进行。此外还可以用成员初始化列表实现对数据成员的初始化。
类名::构造函数名([参数表])[:(成员初始化列表)]
{
//构造函数体
}
- 1
- 2
- 3
- 4
class A{
private:
int x;
int& rx;
const double pi;
public:
A(int v) : x(v), rx(x), pi(3.14) //成员初始化列表
{ }
void print()
{
cout << "x = " << x << " rx = " << rx << " pi = " << pi << endl;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
**说明:**类成员是按照它们在类里被声明的顺序进行初始化的,与它们在成员初始化列表中列出的顺序无关。
带默认参数的构造函数
#include <iostream>
using namespace std;
class Score{
public:
Score(int m = 0, int f = 0); //带默认参数的构造函数
void setScore(int m, int f);
void showScore();
private:
int mid_exam;
int fin_exam;
};
Score::Score(int m, int f) : mid_exam(m), fin_exam(f)
{
cout << “构造函数使用中…” << endl;
}
void Score::setScore(int m, int f)
{
mid_exam = m;
fin_exam = f;
}
void Score::showScore()
{
cout << "期中成绩: " << mid_exam << endl;
cout << “期末成绩:” << fin_exam << endl;
}
int main()
{
Score op1(99, 100);
Score op2(88);
Score op3;
op1.showScore();
op2.showScore();
op3.showScore();
<span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
}
- 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
析构函数
析构函数也是一种特殊的成员函数。它执行与构造函数相反的操作,通常用于撤销对象时的一些清理任务,如释放分配给对象的内存空间等。析构函数有以下一些特点:
- 析构函数与构造函数名字相同,但它前面必须加一个波浪号(~)。
- 析构函数没有参数和返回值,也不能被重载,因此只有一个。
- 当撤销对象时,编译系统会自动调用析构函数。
class Score{
public:
Score(int m = 0, int f = 0);
~Score(); //析构函数
private:
int mid_exam;
int fin_exam;
};
Score::Score(int m, int f) : mid_exam(m), fin_exam(f)
{
cout << “构造函数使用中…” << endl;
}
Score::~Score()
{
cout << “析构函数使用中…” << endl;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
**说明:**在以下情况中,当对象的生命周期结束时,析构函数会被自动调用:
- 如果定义了一个全局对象,则在程序流程离开其作用域时,调用该全局对象的析构函数。
- 如果一个对象定义在一个函数体内,则当这个函数被调用结束时,该对象应该被释放,析构函数被自动调用。
- 若一个对象是使用
new
运算符创建的,在使用delete
运算符释放它时,delete
会自动调用析构函数。
如下示例:
#include <iostream>
#include <string>
using namespace std;
class Student{
private:
char name;
char stu_no;
float score;
public:
Student(char name1, char stu_no1, float score1);
~Student();
void modify(float score1);
void show();
};
Student::Student(char name1, char stu_no1, float score1)
{
name = new char[strlen(name1) + 1];
strcpy(name, name1);
stu_no = new char[strlen(stu_no1) + 1];
strcpy(stu_no, stu_no1);
score = score1;
}
Student::~Student()
{
delete []name;
delete []stu_no;
}
void Student::modify(float score1)
{
score = score1;
}
void Student::show()
{
cout << "姓名: " << name << endl;
cout << "学号: " << stu_no << endl;
cout << “成绩:” << score << endl;
}
int main()
{
Student stu(“雪女”, “2020199012”, 99);
stu.modify(100);
stu.show();
<span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
}
- 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
默认的构造函数和析构函数
如果没有给类定义构造函数,则编译系统自动生成一个默认的构造函数。
说明:
-
对没有定义构造函数的类,其公有数据成员可以用初始值列表进行初始化。
class A{ public: char name[10]; int no; };
A a = {
“chen”, 23};
cout << a.name << a.no << endl;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
-
只要一个类定义了一个构造函数(不一定是无参构造函数),系统将不再给它提供默认的构造函数。
每个类必须有一个析构函数。若没有显示地为一个类定义析构函数,编译系统会自动生成一个默认的析构函数。
构造函数的重载
class Score{ public: Score(int m, int f); //构造函数 Score(); void setScore(int m, int f); void showScore(); private: int mid_exam; int fin_exam; };
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
**注意:**在一个类中,当无参数的构造函数和带默认参数的构造函数重载时,有可能产生二义性。
拷贝构造函数
拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。拷贝构造函数的作用是在建立一个新对象时,使用一个已存在的对象去初始化这个新对象。
拷贝构造函数具有以下特点:
- 因为拷贝构造函数也是一种构造函数,所以其函数名与类名相同,并且该函数也没有返回值。
- 拷贝构造函数只有一个参数,并且是同类对象的引用。
- 每个类都必须有一个拷贝构造函数。可以自己定义拷贝构造函数,用于按照需要初始化新对象;如果没有定义类的拷贝构造函数,系统就会自动生成一个默认拷贝构造函数,用于复制出与数据成员值完全相同的新对象。
自定义拷贝构造函数
类名::类名(const 类名 &对象名) { 拷贝构造函数的函数体; }
class Score{
public:
Score(int m, int f); //构造函数
Score();
Score(const Score &p); //拷贝构造函数
~Score(); //析构函数
void setScore(int m, int f);
void showScore();
private:
int mid_exam;
int fin_exam;
};Score::Score(int m, int f)
{
mid_exam = m;
fin_exam = f;
}Score::Score(const Score &p)
{
mid_exam = p.mid_exam;
fin_exam = p.fin_exam;
}调用拷贝构造函数的一般形式为:
类名 对象2(对象1);
类名 对象2 = 对象1;
Score sc1(98, 87);
Score sc2(sc1); //调用拷贝构造函数
Score sc3 = sc2; //调用拷贝构造函数- 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
调用拷贝构造函数的三种情况:
- 当用类的一个对象去初始化该类的另一个对象时;
- 当函数的形参是类的对象,调用函数进行形参和实参结合时;
- 当函数的返回值是对象,函数执行完成返回调用者时。
浅拷贝和深拷贝
浅拷贝,就是由默认的拷贝构造函数所实现的数据成员逐一赋值。通常默认的拷贝构造函数是能够胜任此工作的,但若类中含有
指针类型
的数据,则这种按数据成员逐一赋值的方法会产生错误。class Student{ public: Student(char *name1, float score1); ~Student(); private: char *name; float score; };
如下语句会产生错误
Student stu1(“白”, 89);
Student stu2 = stu1;- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
上述错误是因为stu1和stu2所指的内存空间相同,在析构函数释放stu1所指的内存后,再释放stu2所指的内存会发生错误,因为此内存空间已被释放。解决方法就是重定义拷贝构造函数,为其变量重新生成内存空间。
Student::Student(const Student& stu) { name = new char[strlen(stu.name) + 1]; if (name != 0) { strcpy(name, stu.name); score = stu.score; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
四、类和对象(二)
~
4.1 自引用指针this
this
指针保存当前对象的地址,称为自引用指针。void Sample::copy(Sample& xy) { if (this == &xy) return; *this = xy; }
- 1
- 2
- 3
- 4
- 5
~
4.2 对象数组与对象指针
对象数组
类名 数组名[下标表达式] 用只有一个参数的构造函数给对象数组赋值 Exam ob[4] = {89, 97, 79, 88}; 用不带参数和带一个参数的构造函数给对象数组赋值 Exam ob[4] = {89, 90}; 用带有多个参数的构造函数给对象数组赋值 Score rec[3] = {Score(33, 99), Score(87, 78), Score(99, 100)};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
对象指针
每一个对象在初始化后都会在内存中占有一定的空间。因此,既可以通过对象名访问对象,也可以通过对象地址来访问对象。对象指针就是用于存放对象地址的变量。声明对象指针的一半语法形式为:
类名 *对象指针名
Score score; Score *p; p = &score; p->成员函数();
- 1
- 2
- 3
- 4
用对象指针访问对象数组
Score score[2]; score[0].setScore(90, 99); score[1].setScore(67, 89);
Score *p;
p = score; //将对象score的地址赋值给p
p->showScore();
p++; //对象指针变量加1
p->showSccore();Score *q;
q =&score[1]; //将第二个数组元素的地址赋值给对象指针变量q- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
~
4.3 string类
C++支持两种类型的字符串,第一种是C语言中介绍过的、包括一个结束符’
\0
’(即以NULL
结束)的字符数组,标准库函数提供了一组对其进行操作的函数,可以完成许多常用的字符串操作。C++标准库中声明了一种更方便的字符串类型,即字符串类string,类string提供了对字符串进行处理所需要的操作。使用string类必须在程序的开始包括头文件string,即要有以下语句:
#include <string>
常用的string类运算符如下:
=、+、+=、==、!=、<、<=、>、>=、[](访问下标对应字符)、>>(输入)、<<(输出)
- 1
#include <iostream> #include <string> using namespace std;
int main()
{
string str1 = “ABC”;
string str2(“dfdf”);
string str3 = str1 + str2;
cout<< "str1 = “ << str1 << ” str2 = “ << str2 << ” str3 = " << str3 << endl;
str2 += str2;
str3 += “aff”;
cout << "str2 = “ << str2 << ” str3 = " << str3 << endl;
cout << "str1[1] = “ << str1[1] << ” str1 == str2 ? " << (str1 str2) << endl;
string str = “ABC”;
cout << "str == str1 ? " << (str str1) << endl;
return 0;
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
~
4.4 向函数传递对象
- 使用对象作为函数参数:对象可以作为参数传递给函数,其方法与传递其他类型的数据相同。在向函数传递对象时,是通过“传值调用”的方法传递给函数的。因此,函数中对对象的任何修改均不影响调用该函数的对象(实参本身)。
- 使用对象指针作为函数参数:对象指针可以作为函数的参数,使用对象指针作为函数参数可以实现传值调用,即在函数调用时使实参对象和形参对象指针变量指向同一内存地址,在函数调用过程中,形参对象指针所指的对象值的改变也同样影响着实参对象的值。
- 使用对象引用作为函数参数:在实际中,使用对象引用作为函数参数非常普遍,大部分程序员喜欢使用对象引用替代对象指针作为函数参数。因为使用对象引用作为函数参数不但具有用对象指针做函数参数的优点,而且用对象引用作函数参数将更简单、更直接。
#include <iostream> using namespace std;
class Point{
public:
int x;
int y;
Point(int x1, int y1) : x(x1), y(y1) //成员初始化列表
{ }
int getDistance()
{
return x x + y y;
}
};void changePoint1(Point point) //使用对象作为函数参数
{
point.x += 1;
point.y -= 1;
}void changePoint2(Point *point) //使用对象指针作为函数参数
{
point->x += 1;
point->y -= 1;
}void changePoint3(Point &point) //使用对象引用作为函数参数
{
point.x += 1;
point.y -= 1;
}int main()
{
Point point[3] = { Point(1, 1), Point(2, 2), Point(3, 3)};
Point p = point;
changePoint1(p);
cout << "the distance is " << p[0].getDistance() << endl;
p++;
changePoint2(p);
cout << "the distance is " << p->getDistance() << endl;
changePoint3(point[2]);
cout << "the distance is " << point[2].getDistance() << endl;<span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
}
- 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
~
4.5 静态成员
静态数据成员
在一个类中,若将一个数据成员说明为
static
,则这种成员被称为静态数据成员。与一般的数据成员不同,无论建立多少个类的对象,都只有一个静态数据成员的拷贝。从而实现了同一个类的不同对象之间的数据共享。定义静态数据成员的格式如下:
static 数据类型 数据成员名;
说明:
-
静态数据成员的定义与普通数据成员相似,但前面要加上static关键字。
-
静态数据成员的初始化与普通数据成员不同。静态数据成员初始化应在类外单独进行,而且应在定义对象之前进行。一般在main()函数之前、类声明之后的特殊地带为它提供定义和初始化。
-
静态数据成员属于类(准确地说,是属于类中对象的集合),而不像普通数据成员那样属于某一对象,因此,可以使用“
类名::
”访问静态的数据成员。格式如下:类名::静态数据成员名
。 -
静态数据成员与静态变量一样,是在编译时创建并初始化。它在该类的任何对象被建立之前就存在。因此,共有的静态数据成员可以在对象定义之前被访问。对象定以后,共有的静态数据成员也可以通过对象进行访问。其访问格式如下
对象名.静态数据成员名; 对象指针->静态数据成员名;
- 1
- 2
静态成员函数
在类定义中,前面有static说明的成员函数称为静态成员函数。静态成员函数属于整个类,是该类所有对象共享的成员函数,而不属于类中的某个对象。静态成员函数的作用不是为了对象之间的沟通,而是为了处理静态数据成员。定义静态成员函数的格式如下:
static 返回类型 静态成员函数名(参数表)
;与静态数据成员类似,调用公有静态成员函数的一般格式有如下几种:
类名::静态成员函数名(实参表); 对象.静态成员函数名(实参表); 对象指针->静态成员函数名(实参表);
- 1
- 2
- 3
一般而言,静态成员函数不访问类中的非静态成员。若确实需要,静态成员函数只能通过对象名(或对象指针、对象引用)访问该对象的非静态成员。
下面对静态成员函数的使用再做几点说明:
- 一般情况下,静态函数成员主要用来访问静态成员函数。当它与静态数据成员一起使用时,达到了对同一个类中对象之间共享数据的目的。
- 私有静态成员函数不能被类外部的函数和对象访问。
- 使用静态成员函数的一个原因是,可以用它在建立任何对象之前调用静态成员函数,以处理静态数据成员,这是普通成员函数不能实现的功能
- 编译系统将静态成员函数限定为内部连接,也就是说,与现行文件相连接的其他文件中的同名函数不会与该函数发生冲突,维护了该函数使用的安全性,这是使用静态成员函数的另一个原因。
- 静态成员函数是类的一部分,而不是对象的一部分。如果要在类外调用公有的静态成员函数,使用如下格式较好:
类名::静态成员函数名()
#include <iostream> using namespace std;
class Score{
private:
int mid_exam;
int fin_exam;
static int count; //静态数据成员,用于统计学生人数
static float sum; //静态数据成员,用于统计期末累加成绩
static float ave; //静态数据成员,用于统计期末平均成绩
public:
Score(int m, int f);
~Score();
static void show_count_sum_ave(); //静态成员函数
};Score::Score(int m, int f)
{
mid_exam = m;
fin_exam = f;
++count;
sum += fin_exam;
ave = sum / count;
}Score::~Score()
{}
/*** 静态成员初始化 ***/
int Score::count = 0;
float Score::sum = 0.0;
float Score::ave = 0.0;void Score::show_count_sum_ave()
{
cout << "学生人数: " << count << endl;
cout << "期末累加成绩: " << sum << endl;
cout << "期末平均成绩: " << ave << endl;
}int main()
{
Score sco[3] = { Score(90, 89), Score(78, 99), Score(89, 88)};
sco[2].show_count_sum_ave();
Score::show_count_sum_ave();<span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
}
- 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
~
4.6 友元
类的主要特点之一是数据隐藏和封装,即类的私有成员(或保护成员)只能在类定义的范围内使用,也就是说私有成员只能通过它的成员函数来访问。但是,有时为了访问类的私有成员而需要在程序中多次调用成员函数,这样会因为频繁调用带来较大的时间和空间开销,从而降低程序的运行效率。为此,C++提供了友元来对私有或保护成员进行访问。友元包括友元函数和友元类。
友元函数
友元函数既可以是不属于任何类的非成员函数,也可以是另一个类的成员函数。友元函数不是当前类的成员函数,但它可以访问该类的所有成员,包括私有成员、保护成员和公有成员。
在类中声明友元函数时,需要在其函数名前加上关键字friend。此声明可以放在公有部分,也可以放在保护部分和私有部分。友元函数可以定义在类内部,也可以定义在类外部。
1、将非成员函数声明为友元函数
#include <iostream> using namespace std; class Score{ private: int mid_exam; int fin_exam; public: Score(int m, int f); void showScore(); friend int getScore(Score &ob); };
Score::Score(int m, int f)
{
mid_exam = m;
fin_exam = f;
}int getScore(Score &ob)
{
return (int)(0.3 ob.mid_exam + 0.7 ob.fin_exam);
}int main()
{
Score score(98, 78);
cout << "成绩为: " << getScore(score) << endl;<span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
}
- 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
说明:
- 友元函数虽然可以访问类对象的私有成员,但他毕竟不是成员函数。因此,在类的外部定义友元函数时,不必像成员函数那样,在函数名前加上“
类名::
”。 - 因为友元函数不是类的成员,所以它不能直接访问对象的数据成员,也不能通过this指针访问对象的数据成员,它必须通过作为入口参数传递进来的对象名(或对象指针、对象引用)来访问该对象的数据成员。
- 友元函数提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。尤其当一个函数需要访问多个类时,友元函数非常有用,普通的成员函数只能访问其所属的类,但是多个类的友元函数能够访问相关的所有类的数据。
例子:一个函数同时定义为两个类的友元函数
#include <iostream> #include <string> using namespace std;
class Score; //对Score类的提前引用说明
class Student{
private:
string name;
int number;
public:
Student(string na, int nu) {
name = na;
number = nu;
}
friend void show(Score &sc, Student &st);
};class Score{
private:
int mid_exam;
int fin_exam;
public:
Score(int m, int f) {
mid_exam = m;
fin_exam = f;
}
friend void show(Score &sc, Student &st);
};void show(Score &sc, Student &st) {
cout << “姓名:” << st.name << " 学号:“ << st.number << endl;
cout << “期中成绩:” << sc.mid_exam << ” 期末成绩:" << sc.fin_exam << endl;
}int main() {
Score sc(89, 99);
Student st(“白”, 12467);
show(sc, st);<span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
}
- 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
2、将成员函数声明为友元函数
一个类的成员函数可以作为另一个类的友元,它是友元函数中的一种,称为友元成员函数。友元成员函数不仅可以访问自己所在类对象中的私有成员和公有成员,还可以访问friend声明语句所在类对象中的所有成员,这样能使两个类相互合作、协调工作,完成某一任务。
#include <iostream> #include <string> using namespace std;
class Score; //对Score类的提前引用说明
class Student{
private:
string name;
int number;
public:
Student(string na, int nu) {
name = na;
number = nu;
}
void show(Score &sc);
};class Score{
private:
int mid_exam;
int fin_exam;
public:
Score(int m, int f) {
mid_exam = m;
fin_exam = f;
}
friend void Student::show(Score &sc);
};void Student::show(Score &sc) {
cout << “姓名:” << name << " 学号:“ << number << endl;
cout << “期中成绩:” << sc.mid_exam << ” 期末成绩:" << sc.fin_exam << endl;
}int main() {
Score sc(89, 99);
Student st(“白”, 12467);
st.show(sc);<span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
}
- 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
说明:
- 一个类的成员函数作为另一个类的友元函数时,必须先定义这个类。并且在声明友元函数时,需要加上成员函数所在类的类名;
友元类
可以将一个类声明为另一个类的友元
class Y{ ··· }; class X{ friend Y; //声明类Y为类X的友元类 };
- 1
- 2
- 3
- 4
- 5
- 6
当一个类被说明为另一个类的友元类时,它所有的成员函数都成为另一个类的友元函数,这就意味着作为友元类中的所有成员函数都可以访问另一个类中的所有成员。
友元关系不具有交换性和传递性。
~
4.7 类的组合
在一个类中内嵌另一个类的对象作为数据成员,称为类的组合。该内嵌对象称为对象成员,又称为子对象。
class Y{ ··· }; class X{ Y y; ··· };
- 1
- 2
- 3
- 4
- 5
- 6
- 7
~
4.8 共享数据的保护
常类型的引入就是为了既保护数据共享又防止数据被改动。常类型是指使用类型修饰符const说明的类型,常类型的变量或对象成员的值在程序运行期间是不可改变的。
常引用
如果在说明引用时用const修饰,则被说明的引用为常引用。常引用所引用的对象不能被更新。如果用常引用做形参,便不会产生对实参的不希望的更改。
const 类型& 引用名
int a = 5; const int& b = a; 此时再对b赋值是非法的。 --------------------------- int add(const int& m, const int& n) { return m + n; } 在此函数中对变量m和变量n更新时非法的
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
常对象
如果在说明对象时用const修饰,则被说明的对象为常对象。常对象中的数据成员为常量且必须要有初值。
类名 const 对象名[(参数表)];
const Date date(2021, 5, 31);
- 1
常对象成员
1、常数据成员
类的数据成员可以是常量或常引用,使用const说明的数据成员称为常数据成员。如果在一个类中说明了常数据成员,那么构造函数就只能通过成员初始化列表对该数据成员进行初始化,而任何其他函数都不能对该成员赋值。
class Date{ private: int year; int month; int day; public: Date(int y, int m, int d) : year(y), month(m), day(d) {
<span class="token punctuation">}</span>
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
一旦某对象的常数据成员初始化后,该数据成员的值是不能改变的。
2、常成员函数
类型 函数名(参数表) const;
const是函数类型的一个组成部分,因此在声明函数和定义函数时都要有关键字const。在调用时不必加const。
class Date{ private: int year; int month; int day; public: Date(int y, int m, int d) : year(y), month(m), day(d){
<span class="token punctuation">}</span> <span class="token keyword">void</span> <span class="token function">showDate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">void</span> <span class="token function">showDate</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">const</span><span class="token punctuation">;</span>
};
void Date::showDate() {
//···
}void Date::showDate() const {
//···
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
关键字const可以被用于对重载函数进行区分。
说明:
- 常成员函数可以访问常数据成员,也可以访问普通数据成员。
- 常对象只能调用它的常成员对象,而不能调用普通成员函数。常成员函数是常对象唯一的对外接口。
- 常对象函数不能更新对象的数据成员,也不能调用该类的普通成员函数,这就保证了在常成员函数中绝不会更新数据成员的值。
五、继承与派生
继承可以在已有类的基础上创建新的类,新类可以从一个或多个已有类中继承成员函数和数据成员,而且可以重新定义或加进新的数据和函数,从而形成类的层次或等级。其中,已有类称为基类或父类,在它基础上建立的新类称为派生类或子类。
~
5.1 继承与派生的概念
类的继承是新的类从已有类那里得到已有的特性。从另一个角度来看这个问题,从已有类产生新类的过程就是类的派生。类的继承和派生机制较好地解决了代码重用的问题。
关于基类和派生类的关系,可以表述为:派生类是基类的具体化,而基类则是派生类的抽象。
使用继承的案例如下:
#include <iostream> #include <string> using namespace std;
class Person{
private:
string name;
string id_number;
int age;
public:
Person(string name1, string id_number1, int age1) {
name = name1;
id_number = id_number1;
age = age1;
}
~Person() {<span class="token punctuation">}</span> <span class="token keyword">void</span> <span class="token function">show</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> cout <span class="token operator"><<</span> <span class="token string">"姓名: "</span> <span class="token operator"><<</span> name <span class="token operator"><<</span> <span class="token string">" 身份证号: "</span> <span class="token operator"><<</span> id_number <span class="token operator"><<</span> <span class="token string">" 年龄: "</span> <span class="token operator"><<</span> age <span class="token operator"><<</span> endl<span class="token punctuation">;</span> <span class="token punctuation">}</span>
};
class Student:public Person{
private:
int credit;
public:
Student(string name1, string id_number1, int age1, int credit1):Person(name1, id_number1, credit1) {
credit = credit1;
}
~Student() {<span class="token punctuation">}</span> <span class="token keyword">void</span> <span class="token function">show</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> <span class="token class-name">Person</span><span class="token double-colon punctuation">::</span><span class="token function">show</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> cout <span class="token operator"><<</span> <span class="token string">"学分: "</span> <span class="token operator"><<</span> credit <span class="token operator"><<</span> endl<span class="token punctuation">;</span> <span class="token punctuation">}</span>
};
int main() {
Student stu(“白”, “110103**********23”, 12, 123);
stu.show();<span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
}
- 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
从已有类派生出新类时,可以在派生类内完成以下几种功能:
- 可以增加新的数据成员和成员函数
- 可以对基类的成员进行重定义
- 可以改变基类成员在派生类中的访问属性
基类成员在派生类中的访问属性
派生类可以继承基类中除了构造函数与析构函数之外的成员,但是这些成员的访问属性在派生过程中是可以调整的。从基类继承来的成员在派生类中的访问属性也有所不同。
基类中的成员 继承方式 基类在派生类中的访问属性 private public|protected|private 不可直接访问 public public|protected|private public|protected|private protected public|protected|private protected|protected|private 派生类对基类成员的访问规则
基类的成员可以有public、protected、private3中访问属性,基类的成员函数可以访问基类中其他成员,但是在类外通过基类的对象,就只能访问该基类的公有成员。同样,派生类的成员也可以有public、protected、private3种访问属性,派生类的成员函数可以访问派生类中自己增加的成员,但是在派生类外通过派生类的对象,就只能访问该派生类的公有成员。
派生类对基类成员的访问形式主要有以下两种:
- 内部访问:由派生类中新增的成员函数对基类继承来的成员的访问。
- 对象访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问。
~
5.2 派生类的构造函数和析构函数
构造函数的主要作用是对数据进行初始化。在派生类中,如果对派生类新增的成员进行初始化,就需要加入派生类的构造函数。与此同时,对所有从基类继承下来的成员的初始化工作,还是由基类的构造函数完成,但是基类的构造函数和析构函数不能被继承,因此必须在派生类的构造函数中对基类的构造函数所需要的参数进行设置。同样,对撤销派生类对象的扫尾、清理工作也需要加入新的析构函数来完成。
调用顺序
#include <iostream> #include <string> using namespace std;
class A{
public:
A() {
cout << “A类对象构造中…” << endl;
}
~A() {
cout << “析构A类对象…” << endl;
}
};class B : public A{
public:
B() {
cout << “B类对象构造中…” << endl;
}
~B(){
cout << “析构B类对象…” << endl;
}
};int main() {
B b;
return 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
代码运行结果如下:
A类对象构造中... B类对象构造中... 析构B类对象... 析构A类对象...
- 1
- 2
- 3
- 4
可见:构造函数的调用严格地按照先调用基类的构造函数,后调用派生类的构造函数的顺序执行。析构函数的调用顺序与构造函数的调用顺序正好相反,先调用派生类的析构函数,后调用基类的析构函数。
派生类构造函数和析构函数的构造规则
派生类构造函数的一般格式为: 派生类名(参数总表):基类名(参数表) { 派生类新增数据成员的初始化语句 } ----------------------------------------------------------------- 含有子对象的派生类的构造函数: 派生类名(参数总表):基类名(参数表0),子对象名1(参数表1),...,子对象名n(参数表n) { 派生类新增成员的初始化语句 }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
在定义派生类对象时,构造函数的调用顺序如下:
调用基类的构造函数,对基类数据成员初始化。
调用子对象的构造函数,对子对象的数据成员初始化。
调用派生类的构造函数体,对派生类的数据成员初始化。
说明:
- 当基类构造函数不带参数时,派生类不一定需要定义构造函数;然而当基类的构造函数哪怕只带有一个参数,它所有的派生类都必须定义构造函数,甚至所定义的派生类构造函数的函数体可能为空,它仅仅起参数的传递作用。
- 若基类使用默认构造函数或不带参数的构造函数,则在派生类中定义构造函数时可略去“
:基类构造函数名(参数表)
”,此时若派生类也不需要构造函数,则可不定义构造函数。 - 如果派生类的基类也是一个派生类,每个派生类只需负责其直接基类数据成员的初始化,依次上溯。
~
5.3 调整基类成员在派生类中的访问属性的其他方法
派生类可以声明与基类成员同名的成员。在没有虚函数的情况下,如果在派生类中定义了与基类成员同名的成员,则称派生类成员覆盖了基类的同名成员,在派生类中使用这个名字意味着访问在派生类中声明的成员。为了在派生类中使用与基类同名的成员,必须在该成员名之前加上基类名和作用域标识符“
::
”,即基类名::成员名
访问声明
访问声明的方法就是把基类的保护成员或共有成员直接写在私有派生类定义式中的同名段中,同时给成员名前冠以基类名和作用域标识符“::”。利用这种方法,该成员就成为派生类的保护成员或共有成员了。
class B:private A{ private: int y; public: B(int x1, int y1) : A(x1) { y = y1; } A::show; //访问声明 };
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
访问声明在使用时应注意以下几点:
- 数据成员也可以使用访问声明。
- 访问声明中只含不带类型和参数的函数名或变量名。
- 访问声明不能改变成员在基类中的访问属性。
- 对于基类的重载函数名,访问声明将对基类中所有同名函数其起作用。
~
5.4 多继承
声明多继承派生类的一般形式如下:
class 派生类名:继承方式1 基类名1,...,继承方式n 基类名n { 派生类新增的数据成员和成员函数 }; 默认的继承方式是private
- 1
- 2
- 3
- 4
多继承派生类的构造函数与析构函数:
与单继承派生类构造函数相同,多重继承派生类构造函数必须同时负责该派生类所有基类构造函数的调用。
多继承构造函数的调用顺序与单继承构造函数的调用顺序相同,也是遵循先调用基类的构造函数,再调用对象成员的构造函数,最后调用派生类构造函数的原则。析构函数的调用与之相反。
~
5.5 虚基类
虚基类的作用:如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最低层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。在访问这些同名成员时,必须在派生类对象名后增加直接基类名,使其唯一地标识一个成员,以免产生二义性。
#include <iostream> #include <string> using namespace std;
class Base{
protected:
int a;
public:
Base(){
a = 5;
cout << "Base a = " << a << endl;
}
};class Base1: public Base{
public:
Base1() {
a = a + 10;
cout << "Base1 a = " << a << endl;
}
};class Base2: public Base{
public:
Base2() {
a = a + 20;
cout << "Base2 a = " << a << endl;
}
};class Derived: public Base1, public Base2{
public:
Derived() {
cout << "Base1::a = " << Base1::a << endl;
cout << "Base2::a = " << Base2::a << endl;
}
};int main() {
Derived obj;
return 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
代码执行结果如下
Base a = 5 Base1 a = 15 Base a = 5 Base2 a = 25 Base1::a = 15 Base2::a = 25
- 1
- 2
- 3
- 4
- 5
- 6
虚基类的声明:
不难理解,如果在上列中类base只存在一个拷贝(即只有一个数据成员a),那么对a的访问就不会产生二义性。在C++中,可以通过将这个公共的基类声明为虚基类来解决这个问题。这就要求从类base派生新类时,使用关键字
virtual
将base声明为虚基类。声明虚基类的语法形式如下:
class 派生类:virtual 继承方式 类名{ ····· };
- 1
- 2
- 3
上述代码修改如下:
class Base1:virtual public Base{ public: Base1() { a = a + 10; cout << "Base1 a = " << a << endl; } };
class Base2:virtual public Base{
public:
Base2() {
a = a + 20;
cout << "Base2 a = " << a << endl;
}
};- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
运行结果如下:
Base a = 5 Base1 a = 15 Base2 a = 35 Base1::a = 35 Base2::a = 35
- 1
- 2
- 3
- 4
- 5
虚基类的初始化:
虚基类的初始化与一般的多继承的初始化在语法上是一样的,但构造函数的调用顺序不同。在使用虚基类机制时应该注意以下几点:
- 如果在虚基类中定义有带形参的构造函数,并且没有定义默认形式的构造函数,则整个继承结构中,所有直接或间接的派生类都必须在构造函数的成员初始化列表中列出对虚基类构造函数的调用,以初始化在虚基类中定义的数据成员。
- 建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。该派生类的其他基类对虚基类构造函数的调用都被自动忽略。
- 若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类构造函数。
- 对于多个虚基类,构造函数的执行顺序仍然是先左后右,自上而下。
- 若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类的构造函数。
~
5.6 赋值兼容规则
在一定条件下,不同类型的数据之间可以进行类型转换,如可以将整型数据赋值给双精度型变量。在赋值之前,先把整型数据转换成双精度数据,然后再把它赋给双精度变量。这种不同数据类型之间的自动转换和赋值,称为赋值兼容。在基类和派生类对象之间也存有赋值兼容关系,基类和派生类对象之间的赋值兼容规则是指在需要基类对象的任何地方,都可以用子类的对象代替。
例如,下面声明的两个类:
class Base{ ····· }; class Derived: public Base{ ····· };
- 1
- 2
- 3
- 4
- 5
- 6
根据赋值兼容规则,在基类Base的对象可以使用的任何地方,都可以使用派生类Derived的对象来代替,但只能使用从基类继承来的成员。具体的表现在以下几个方面:
-
派生类对象可以赋值给基类对象,即用派生类对象中从基类继承来的数据成员,逐个赋值给基类对象的数据成员。
Base b; Derived d; b = d;
- 1
- 2
- 3
-
派生类对象可以初始化基类对象的引用。
Derived d; Base &br = d;
- 1
- 2
-
派生类对象的地址可以赋值给指向基类对象的指针。
Derived d; Base *bp = &d;
- 1
- 2
六、多态性与虚函数
多态性是面向对象程序设计的重要特征之一。多态性机制不仅增加了面向对象软件系统的灵活性,进一步减少了冗余信息,而且显著提高了软件的可重用性和可扩充性。多态性的应用可以使编程显得更简洁便利,它为程序的模块化设计又提供了一种手段。
~
6.1 多态性概述
所谓多态性就是不同对象收到相同的消息时,产生不同的动作。这样,就可以用同样的接口访问不同功能的函数,从而实现“一个接口,多种方法”。
从实现的角度来讲,多态可以划分为两类:编译时的多态和运行时的多态。在C++中,多态的实现和连编这一概念有关。所谓连编就是把函数名与函数体的程序代码连接在一起的过程。静态连编就是在编译阶段完成的连编。编译时的多态是通过静态连编来实现的。静态连编时,系统用实参与形参进行匹配,对于同名的重载函数便根据参数上的差异进行区分,然后进行连编,从而实现了多态性。运行时的多态是用动态连编实现的。动态连编时运行阶段完成的,即当程序调用到某一函数名时,才去寻找和连接其程序代码,对面向对象程序设计而言,就是当对象接收到某一消息时,才去寻找和连接相应的方法。
一般而言,编译型语言(如C,Pascal)采用静态连编,而解释型语言(如LISP)采用动态连编。静态连编要求在程序编译时就知道调用函数的全部信息。因此,这种连编类型的函数调用速度快、效率高,但缺乏灵活性;而动态连编方式恰好相反,采用这种连编方式,一直要到程序运行时才能确定调用哪个函数,它降低了程序的运行效率,但增强了程序的灵活性。纯粹的面向对象程序语言由于其执行机制是消息传递,所以只能采用动态连编。C++实际上采用了静态连编和动态连编相结合的方式。
在C++中,编译时多态性主要是通过函数重载和运算符重载实现的;运行时多态性主要是通过虚函数来实现的。
~
6.2 虚函数
虚函数的定义是在基类中进行的,它是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual,从而提供一种接口界面。定义虚函数的方法如下:
virtual 返回类型 函数名(形参表) { 函数体 }
- 1
- 2
- 3
在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。
#include <iostream> #include <string> using namespace std;
class Family{
private:
string flower;
public:
Family(string name = “鲜花”): flower(name) { }
string getName() {
return flower;
}
virtual void like() {
cout << "家人喜欢不同的花: " << endl;
}
};class Mother: public Family{
public:
Mother(string name = “月季”): Family(name) { }
void like() {
cout << “妈妈喜欢” << getName() << endl;
}
};class Daughter: public Family<sp