背景
从事嵌入式多年,一直和mcu打交道,只用到c语言,后来为了学习QT。就得学习c++,市面上很多教程都是从基础开始教。明显不适合我,经过网上大量查找和总结,终于整理成一篇以从事c语言工作者的角度,快速跨入c++工作者的笔记。适合于c语言基础扎实的人学习,可以快速掌握c++。
1 名字空间 namespace
在C++中,为防止程序中的同名名字冲突,我们可以通过运算符::限定某个名字属于哪个名字空间,如下:
#include <cstdio>
namespace first // 名字(称)空间 first
{
int a;
void f(){/*...*/}
int g(){/*...*/}
}
namespace second // 名字(称)空间 second
{
double a;
double f(){/*...*/}
char g;
}
int main ()
{
first::a = 2; // 使用first空间下的变量a
second::a = 6.453; // 使用second空间下的变量a
first::a = first::g()+second::f();
second::a = first::g()+6.453;
printf("%d\n",first::a);
printf("%lf\n",second::a);
return 0;
}
3种使用名字空间X中名字name的方法:
●using namespace X; //引入整个名字空间
●using X::name ; //引入单个名字
●X::name; //程序中加上名字空间前缀,如X::
2 输入输出流库
C++的新的输入输出流库(头文件iostream)将输入输出看成一个流,并用输出运算符 << 和输入运算符 >> 对数据(变量和常量)进行输入输出;其中有cout和cin分别代表标准输出流对象(屏幕窗口)和标准输入流对象(键盘);标准库中的名字都属于标准名字空间std。
#include <iostream> //标准输入输出头文件
#include <cmath> // 数学函数头文件 sin
using namespace std; //引入整个名字空间std中的所有名字
//cout cin都属于名字空间std;
int main() {
double a;
cout << "从键盘输入一个数" << endl;
cin >> a;
a = sin(a);
cout << a;
return 0;
}
3 同名变量
程序块{}内部作用域可定义与外部作用域同名的变量,在该内部作用域里就隐藏了外部变量。for循环语句也能够定义局部变量。
例程1:不同作用域的同名变量
#include <iostream>
using namespace std;
int main ()
{
double a;
cout << "Type a number: ";
cin >> a;
{ // 内部作用域内定义与外部同名的变量
int a = 1; // "int a"隐藏了外部作用域的“double a"
a = a * 10 + 4;
cout << "Local number: " << a << endl;
}
cout << "You typed: " << a << endl; //main作用域的“double a"
return 0;
}
例程2:for循环中的i与外部的i互不影响
// 下述的for循环中的i与外部的i互不影响。
#include <iostream>
using namespace std;
int main (){
int i = 0;
for (int i = 0; i < 4; i++)
{
cout << i << endl;
}
cout << "i contains: " << i << endl;
for (i = 0; i < 4; i++)
{
for (int i = 0; i < 4; i++) // we're between
{ // previous for's hooks
cout << i<< " ";
}
cout << endl;
}
return 0;
}
4 全局作用域限定::
想要在内部作用域访问同名的全局变量,需要使用到全局作用域限定符::。
// 在内部作用域中,访问全局变量a
#include <iostream>
using namespace std;
double a = 128;
int main (){
double a = 256; // 内部作用域的同名变量a
cout << "Local a: " << a << endl;
cout << "Global a: " <<::a << endl; //::是全局作用域限定
return 0;
}
5 引用类型
C++中引入了引用类型,即给一个变量取一个别名,别名指代的仍是同一个变量。引用经常用作函数的形参,表示形参和实参实际上是同一个对象,在函数中对形参的修改也就是对实参的修改。引用类型和C中的指针有些类似,具体看下面的例子。
以下面的例子中的三个swap函数为例,介绍C++中的这个“引用类型”:
#include <iostream>
using namespace std;
void swap1(int x, int y) {
cout << "swap函数内交换前:" << x << " " << y << endl;
int t = x; x = y; y = t;
cout << "swap函数内交换后:" << x << " " << y << endl;
}
//x,y得到2个int型变量的指针,x,y本身没有修改
//修改的是x,y 指向的那2个int型变量的内容
void swap2(int *x, int *y) {
cout << "swap函数内交换前:" << *x << " " << *y << endl;
int t = *x; *x = *y; *y = t;
cout << "swap函数内交换后:" << *x << " " << *y << endl;
}
//x,y是实参的引用
void swap3(int &x, int &y) {
cout << "swap函数内交换前:" << x << " " << y << endl;
int t = x; x = y; y = t;
cout << "swap函数内交换后:" << x << " " << y << endl;
}
int main()
{
int a, b;
// swap1未能互换a b的数值
a = 3, b = 4;
swap1(a, b);
cout << a << ", " << b << endl; // display: 3, 4
// 指针:swap2成功互换a b的数值
// x,y分别是int*指针,指向a,b
// *x,*y就是a和b,其在swap2函数中互换
a = 3, b = 4;
swap2(&a, &b);
cout << a << ", " << b << endl; // display: 4, 3
// 引用:swap3成功互换a b的数值
// x,y是实参a,b的引用,x,y就是a,b
a = 3, b = 4;
swap3(a, b);
cout << a << ", " << b << endl; // display: 4, 3
return 0;
}
当实参占据内存大时,用引用代替传值(需要复制)可提高效率,如果不希望因此无意中修改实参,可以用const修改符,如下:
#include <iostream>
using namespace std;
void change (double &x, const double &y,double z){
x = 100;
y = 200; //会报错! y不可修改,是const double& 可去掉const
z = 300;
}
int main (){
double a,b,c; //内在类型变量未提供初始化式,默认初始化为0
change(a, b, c);
cout << a << ", " << b << ", " << c << endl;
return 0;
}
6 inline内联函数
对于不包含循环的简单函数,建议用inline关键字声明为"inline内联函数",编译器将内联函数调用用其代码展开,称为“内联展开”,避免函数调用开销,提高程序执行效率。inline关键字只适用于比较简短的函数,如果太长太复杂,就没有使用意义了。
#include <iostream>
#include <cmath>
using namespace std;
inline double distance(double a, double b) {
return sqrt(a * a + b * b);
}
int main() {
double k = 6, m = 9;
// 下面2行将产生同样的代码:
cout << distance(k, m) << endl; // 编译器将直接 替换distance()为sqrt(xx)
cout << sqrt(k * k + m * m) << endl;
return 0;
}
7 try-catch处理异常
在C++中,try-catch语句是用来处理程序中可能发生的异常的机制。当程序执行到可能抛出异常的代码时,这部分代码被放置在try块中。如果try块内的代码抛出了异常,那么程序的控制流会立即跳转到紧随其后的catch块(如果有多个catch块,则跳转到与抛出的异常类型相匹配的第一个catch块)。如果没有任何catch块能够处理这个异常,程序将会终止。
#include <iostream>
#include <stdexcept> // 包含std::runtime_error
int main() {
try {
// 假设这里有一些可能抛出异常的代码
// 例如,我们故意抛出一个运行时错误
throw std::runtime_error("发生了运行时错误!");
// 如果上面的代码没有抛出异常,则try块内的剩余代码将继续执行
} catch (const std::runtime_error& e) {
// 如果try块内抛出了std::runtime_error类型的异常,则执行这里
std::cerr << "捕获到异常: " << e.what() << std::endl;
// 在这里处理异常,例如记录日志、清理资源等
} catch (...) {
// 这个catch块会捕获所有未被前面catch块捕获的异常
// 这是一个"捕获所有"的catch块
std::cerr << "捕获到未知类型的异常" << std::endl;
// 谨慎使用,因为它会捕获所有类型的异常,包括不应该被捕获的
}
// 无论是否捕获到异常,程序都会继续执行到这里
std::cout << "程序继续执行。" << std::endl;
return 0;
}
8 默认形参
函数的形参可带有默认值,但必须一律在最右边。有了默认值,调用该函数时便可以选择性输入带默认值的参数。
#include <iostream>
using namespace std;
double test(double a, double b = 7) {
return a - b;
}
int main() {
cout << test(14, 5) << endl;
cout << test(14) << endl;
return 0;
}
/*错: 默认参数一律靠右*/
// double test(double a, double b = 7, int c) {
// return a - b;
// }
9 函数重载
C++允许函数同名,只要它们的形参不一样(个数或对应参数类型),调用函数时将根据实参和形参的匹配选择最佳函数。如果有多个难以区分的最佳函数,编译器会报错!
注意:不能根据返回类型区分同名函数 (下面的例子中有说明)
#include <iostream>
using namespace std;
double add(double a, double b) {
return a + b;
}
int add(int a, int b) {
return a + b;
}
// 错:编译器无法区分int add (int a, int b)和void add (int a, int b )
// 即,【不能根据返回类型区分同名函数】
// void add(int a, int b) {
// return a - b;
// }
int main() {
double m = 7, n = 4;
int k = 5, p = 3;
cout << add(m, n) << " , " << add(k, p) << endl;
return 0;
}
10 运算符重载
可以使用关键字operator编写重载函数,对基本运算符 +、-、*进行重载。比如,这样可实现直接使用符号+实现字符串的加法运算(string)。重载函数的具体内容需要自己编写程序实现。关于字符串的运算符重载,后续讲解string时涉及。
例程1:对结构体进行运算符重载,实现结构体的“加法、乘法运算”
// 对结构体进行运算符重载,实现结构体的“加法、乘法运算”
#include <iostream>
using namespace std;
// 定义一个结构体Vector2
struct Vector2{
double x;
double y;
};
// 定义结构体Vector2的*运算,返回 数值a*结构体对象b
Vector2 operator * (double a, Vector2 b){
Vector2 r; // 函数内容即为 * 的实现
r.x = a * b.x;
r.y = a * b.y;
return r; // 返回a*b的结果
}
// 定义结构体Vector2的+运算,返回 结构体对象a+结构体对象b
Vector2 operator+ (Vector2 a, Vector2 b) {
Vector2 r; // 函数内容即为 + 的实现
r.x = a.x + b.x;
r.y = a.y + b.y;
return r; // 返回a+b的结果
}
int main (){
Vector2 k, m; // C++定义的struct类型前不需要再加关键字struct
// c定义的得加,即 struct Vector2 k, m;
k.x = 2; //用成员访问运算符.访问成员
k.y = -1;
m = 3.1415927 * k; // Magic!
//也可以使用 m = Vector2 operator *(3.1415927, k);
cout << "(" << m.x << ", " << m.y << ")" << endl;
Vector2 n = m + k;
cout << "(" << n.x << ", " << n.y << ")" << endl;
return 0;
}
例程2:对结构体进行输出运算符 << 重载
#include <iostream>
using namespace std;
struct Vector2 {
double x;
double y;
};
// 针对结构体Vector2 重载输出运算符 <<
ostream& operator << (ostream& o, Vector2 a){
o << "(" << a.x << ", " << a.y << ")";
return o;
}
int main (){
Vector2 a;
a.x = 35;
a.y = 23;
cout << a << endl; // operator <<(cout,a);
return 0;
}
11 函数模板template
我们平时写课程小论文、写实验报告时,老师们一般都会要求我们按照给定的模板来。而函数模板的作用与这里的写作模板是类似的,我们将通过下面的比较大小函数的编写,演示函数模板的作用,使用关键字template。
除了函数的模板,还有类的模板,这将在后文中介绍。下面编写了两个分别用于比较两个int和double型数据大小的函数。
#include <iostream>
using namespace std;
int minValue(int a, int b) {//return a<b?a:b
if (a < b) return a;
else return b;
}
double minValue(double a, double b) {//return a<b?a:b
if (a < b) return a;
else return b;
}
int main() {
int i = 3, j = 4;
cout << "min of " << i << " and " << j << " is " << minValue(i, j) << endl;
double x = 3.5, y = 10;
cout << "min of " << x << " and " << y << " is " << minValue(x, y) << endl;
}
我们可以可以发现,上面编写的int和double型的比较函数,仅是数据类型不一样,其函数内容是一致的。我们可以利用函数模板template,改写上述程序内容如下:
#include <iostream>
using namespace std;
// 定义一个函数模板如下 (定义了一个通用数据类型TT)
// TT本身并未指定数据类型 (TT也可为PP、XX...)
// 调用该函数模板时,编译器将会自动生成一个针对该数据类型的函数
template<class TT> // 固定格式 template<class name>
TT minValue(TT a, TT b) {
if (a < b) return a;
else return b;
}
int main() {
int i = 3, j = 4;
cout << "min of " << i << " and " << j << " is " << minValue(i, j) << endl; // 编译器会自动将TT替换为int
double x = 3.5, y = 10;
cout << "min of " << x << " and " << y << " is " << minValue(x, y) << endl; // 编译器会自动将TT替换为double
// cout << "min of " << i << " and " << y << " is " << minValue(i, y) << endl; // 不同类型比较会报错,因为编译器不知道该将 TT替换为什么。
}
上面的模板函数可以轻松实现两个相同类型数据的大小比较,但是并不能进行两个不同类型数据的大小比较(会报错)。针对该问题,我们可以定义两个通用数据类型,如下(定义更多也没问题):
#include <iostream>
using namespace std;
// 定义函数模板如下(定义了两个通用数据类型)
// TT本身并未指定数据类型 (TT也可为PP、XX...)
// 调用该函数模板时,编译器将会自动生成一个针对该数据类型的函数
template<class T1, class T2> // 固定格式 template<class name1, class name2,...>
T1 minValue(T1 a, T2 b) { // 返回值的数据类型,得选一个
if (a < b) return a;
else return b;
}
int main() {
int i = 3, j = 4;
cout << "min of " << i << " and " << j << " is " << minValue(i, j) << endl; // 编译器会自动将T1 T2替换为int
double x = 3.5, y = 10;
cout << "min of " << x << " and " << y << " is " << minValue(x, y) << endl; // 编译器会自动将T1 T2替换为double
cout << "min of " << i << " and " << y << " is " << minValue(i, y) << endl; // 编译器会自动将T1 T2进行替换
}
12 动态内存分配
该部分提到了“构造函数”和“析构函数”,暂时可以不用管这具体是什么,后面会介绍。C++中使用关键字new和delete进行内存的分配与删除,C中使用的是malloc/alloc/realloc和free。关键字new和delete可以对类对象调用初始化构造函数或销毁析构函数。使用完new进行内存分配后,不能忘记使用delete释放内存。
下面的例程中会涉及到指针相关内容,可以先看看下面的关于指针的小例程,然后再继续查看new和delete的例程。该部分例程中包含了许多指针内容,如果对指针比较熟悉,没必要看完给出的几个例子。
#include <iostream>
#include <cstring>
using namespace std;
int main() {
double d = 3.14; // 变量d是一块存放double值的内存块
double *dp; // 指针变量dp:保存double类型的地址的变量
// dp值的类型是double *
// dp是存放double *类型值 的内存块
dp = &d; //取地址运算符&用于获得一个变量的地址
// 将double变量d的地址(指针)保存到double*指针变量dp中
// dp和&d的类型都是double *
*dp = 4.14; //解引用运算符* 用于获得指针变量指向的那个变量(C++中也称为对象)
//*dp就是dp指向的那个d
cout << "*dp= " << *dp << " d=:" << d << endl;
cout << "Type a number: ";
cin >> *dp; //输出dp指向的double内存块的值
cout << "*dp= " << *dp << " d=:" << d << endl;
return 0;
}
如下为关于new和delete的例程1:
#define _CRT_SECURE_NO_WARNINGS //windows
#include <iostream>
#include <cstring>
using namespace std;
int main() {
double d = 3.14; // 变量d是一块存放double值的内存块
double *dp; // 指针变量dp:保存double类型的地址 的变量
dp = new double; // new 分配正好容纳double值的内存块(如4或8个字节)
// 并返回这个内存块的地址,而且地址的类型是double *
// 这个地址被保存在dp中,dp指向这个新内存块,不再是原来d那个内存块
// 但目前这个内存块的值是未知的
// 注意:
// new 分配的是堆存储空间,即所有程序共同拥有的自由内存空间
// 而d,dp等局部变量是这个程序自身的静态存储空间
// new会对这个double元素调用double类型的构造函数做初始化,比如初始化为0
*dp = 45.3; //*dp指向的double内存块的值变成45.3
cout << "Type a number: ";
cin >> *dp; //输出dp指向的double内存块的值
cout << "*dp= " << *dp << endl;
*dp = *dp + 5; //修改dp指向的double内存块的值45.3+5
cout << "*dp= " << *dp << endl;
delete dp; // delete 释放dp指向的动态分配的double内存块
dp = new double[5]; //new 分配可以存放15个double值的内存块
//返回这块连续内存的起始地址,而且指针类型是double *
//实际是第一个double元素的地址
// new会对每个double元素调用double类型的构造函数做初始化,比如初始化为0
dp[0] = 4456; // dp[0]等价于 *(dp+0)即*dp,也即是第1个double元素的内存块
dp[1] = dp[0] + 567; // dp[1]等价于 *(dp+1),也即是第2个double元素的内存块
cout << "d[0]=: " << dp[0] << " d[1]=: " << dp[1] << endl;
delete[] dp; // 释放dp指向的多个double元素占据的内存块
// 对每个double元素调用析构函数以释放资源
// 注意:缺少[],只释放第一个double元素的内存块,会造成“内存泄漏”
return 0;
}
如下为关于new和delete的例程2:
#define _CRT_SECURE_NO_WARNINGS //windows
#include <iostream>
#include <cstring>
using namespace std;
int main() {
double *dp; // 指针变量dp:保存double类型的地址 的变量
int n = 8;
dp = new double[n]; // new 可以分配随机大小的double元素,
// 而静态数组则必须是编译器固定大小,即大小为常量
// 如 double arr[20];
// 通过下标访问每个元素
for (int i = 0; i < n; i++) {
dp[i] = i;
} //通过指针访问每个元素
double *p = dp; // 取得 dp的地址
for (int i = 0; i < n; i++) {
cout << *(p + i) << endl; //p[i]或dp[i]
}
cout << endl;
// *p指向dp,取得dp的地址 q为dp的末位元素的地址
for (double *p = dp, *q = dp + n; p < q; p++) {
cout << *p << endl;
}
cout << endl;
delete[] dp;
char *s;
s = new char[100];
strcpy(s, "Hello!"); //将字符串常量拷贝到s指向的字符数组内存块中
cout << s << endl;
delete[] s; //用完以后,记得释放内存块,否则会“内存泄漏”!
return 0;
}
13 类的成员函数
类是在C的结构体struct类型的基础上,增加了成员函数。C的strcut可将一个概念或实体的所有属性组合在一起,描述同一类对象的共同属性。C++使得struct不但包含数据,还包含函数(方法)用于访问或修改类变量(对象)的这些属性。从此将借助几个例程,介绍类中成员函数的外部定义形式、成员函数自引用、成员函数重载运算符。
C++的struct和class都可以包含函数,但二者存在着明显区别。关键字struct和class的使用区别,主要就在于访问限定符。
例程1:可以在类的外部定义成员函数(但必须在类的内部声明),外部定义时,需要在函数名前加上类的作用域 【类名::】。
#include <iostream>
using namespace std;
struct Date {
int d, m, y;
void init(int dd, int mm, int yy) {
d = dd; m = mm; y = yy;
}
void print();
};
// 格式:【返回值类型 类名::函数名() 】
void Date::print() {
cout << y << "-" << m << "-" << d << endl;
}
int main (){
Date day; // 实例化一个对象
day.print(); //通过类Date对象day调用类Date的print方法
day.init(4, 6, 1999); //通过类Date对象day调用类Date的init方法
day.print(); //通过类Date对象day调用类Date的print方法
return 0;
}
例程2:成员函数 返回 “自引用” (*this)
// 成员函数 返回 “自引用” (*this)
#include <iostream>
using namespace std;
struct Date {
int d, m, y;
void init(int dd, int mm, int yy) {
d = dd; m = mm; y = yy;
}
void print() {
cout << y << "-" << m << "-" << d << endl;
}
// 对 对象本身进行操作,然后返回
Date& add(int dd) {
d = d + dd;
return *this; //this是指向调用这个函数的类型对象指针
// *this就是调用这个函数的那个对象
// 这个成员函数返回的是“自引用”,即调用这个函数的对象本身
// 通过返回自引用,可以连续调用这个函数
// day.add(3);
// day.add(3).add(7);
}
};
int main() {
Date day;
day.print(); //通过类Date对象day调用类Date的print方法
day.init(4, 6, 1999); //通过类Date对象day调用类Date的init方法
day.print(); //通过类Date对象day调用类Date的print方法
day.add(3);
day.add(5).add(7);
day.print();
return 0;
}
例程3:成员函数重载“运算符函数”
这里和前面对结构体进行运算符的重载是类似的。
//成员函数重载“运算符函数”
#include <iostream>
using namespace std;
struct Date {
int d, m, y;
void init(int dd, int mm, int yy) {
d = dd; m = mm; y = yy;
}
void print() {
cout << y << "-" << m << "-" << d << endl;
}
// 类的对象的运算符重载
Date& operator+=(int dd) {
d = d + dd;
return *this; //this是指向调用这个函数的类型对象指针,
// *this就是调用这个函数的那个对象
//这个成员函数返回的是“自引用”,即调用这个函数的对象本身
//通过返回自引用,可以连续调用这个函数
// day.add(3);
// day.add(3).add(7);
}
};
int main() {
Date day;
day.print(); //通过类Date对象day调用类Date的print方法
day.init(4, 6, 1999); //通过类Date对象day调用类Date的init方法
day.print(); //通过类Date对象day调用类Date的print方法
day += 3; // day.add(3);
(day += 5) += 7; //day.add(5).add(7);
day.print();
return 0;
}
14 构造函数和析构函数
构造函数是和类名同名且没有返回类型的函数,在定义对象时会自动被调用。有了构造函数就不需要再单独调用专门的初始化函数咯,如init(x,x,x)。构造函数用于初始化类对象成员,包括申请一些资源,如分配内存、打开某文件等。构造函数可以进行重载,依旧遵循前面的函数重载中的规则。
析构函数是在类对象销毁时被自动调用,用于释放该对象占用的资源,如释放占用的内存、关闭打开的文件。
构造函数: 类名(形参1, 形参2, …) {}
析构函数: ~类名() {}我们在定义一个类的时候,如果没有定义构造函数和析构函数,编译器会自动生成空的构造函数和析构函数。
总结:如果是new的方式,实例化的对象,不管是在什么位置,都要delete。类似与malloc和free。直接构建的方式和变量类似,函数结束会自动释放。
例程1:构造函数与析构函数
#include <iostream>
using namespace std;
struct Date {
int d, m, y;
// 定义构造函数
Date(int dd, int mm, int yy) {
d = dd; m = mm; y = yy;
cout << "构造函数" << endl;
}
void print() {
cout << y << "-" << m << "-" << d << endl;
}
~Date() {// 析构函数名是~和类名,且不带参数,没有返回类型
// 目前不需要做任何释放工作,因为构造函数没申请资源
// new delete
cout << "析构函数" << endl;
}
};
int main(){
// Date day; //编译器会报错!
// 实例化对象时,会自动调用构造函数,这里没有提供需要的3个参数,会报错。
// 为此,可以重载构造函数或者给构造函数的参数设置默认值(后文介绍)
Date day(4, 6, 1999); //会自动调用构造函数Date(int dd, int mm, int yy)
// day.init(4, 6, 1999); //通过类Date对象day调用类Date的init方法(init函数参见前面的例程)
day.print(); //通过类Date对象day调用类Date的print方法
return 0;
}
例程2:构造函数的重载(与前面的函数重载雷同)
#include <iostream>
using namespace std;
struct Date {
int d, m, y;
// 定义构造函数
Date() {
d = m = 1; y = 2000;
cout << "构造函数" << endl;
}
Date(int dd) {
d = dd; m = 1; y = 2000;
cout << "构造函数" << endl;
}
Date(int dd, int mm) {
d = dd; m = mm; y = 2000;
cout << "构造函数" << endl;
}
Date(int dd, int mm, int yy) {
d = dd; m = mm; y = yy;
cout << "构造函数" << endl;
}
void print() {
cout << y << "-" << m << "-" << d << endl;
}
~Date() {// 析构函数名是~和类名,且不带参数,没有返回类型
// 目前不需要做任何释放工作,因为构造函数没申请资源
// new delete
cout << "析构函数" << endl;
}
};
int main(){
// 重载构造函数
Date day0;//自动调用构造函数Date()
Date day1(2);//自动调用构造函数Date(int dd)
Date day2(2,3);//自动调用构造函数Date(int dd, int mm)
Date day3(4, 6, 1999); //自动调用构造函数Date(int dd, int mm, int yy)
day0.print();
day1.print();
day2.print();
day3.print();
return 0;
}
例程3:构造函数使用默认参数
#include <iostream>
using namespace std;
struct Date {
int d, m, y;
// 定义构造函数
Date(int dd = 1, int mm = 2, int yy = 2022) {
d = dd; m = mm; y = yy;
cout << "构造函数" << endl;
}
void print() {
cout << y << "-" << m << "-" << d << endl;
}
~Date() {// 析构函数名是~和类名,且不带参数,没有返回类型
// 目前不需要做任何释放工作,因为构造函数没申请资源
// new delete
cout << "析构函数" << endl;
}
};
int main(){
// 重载构造函数
Date day0;//自动调用构造函数Date()
Date day1(2);//自动调用构造函数Date(int dd)
Date day2(2,3);//自动调用构造函数Date(int dd, int mm)
Date day3(4, 6, 1999); //自动调用构造函数Date(int dd, int mm, int yy)
day0.print();
day1.print();
day2.print();
day3.print();
return 0;
}
例程4:析构函数的使用
virtual是虚函数的关键字,暂时不用管,后文将介绍
//析构函数示例
#define _CRT_SECURE_NO_WARNINGS //windows系统
#include <iostream>
#include <cstring>
using namespace std;
struct student {
char *name;
int age;
// 带默认参数的构造函数
student(const char *n = "no name", int a = 0) {
name = new char[100]; // 比malloc好!
strcpy(name, n);
age = a;
cout << "构造函数,申请了100个char元素的动态空间" << endl;
}
// virtual是虚函数的关键字,暂时不用管,后文将介绍
virtual ~student(){ // 析构函数 new delete
delete name; // 不能用free! 不释放会造成“内存泄漏”
cout << "析构函数,释放了100个char元素的动态空间" << endl;
}
};
int main() {
cout << "Hello!" << endl << endl;
student a;
cout << a.name << ", age " << a.age << endl << endl;
student b("John");
cout << b.name << ", age " << b.age << endl << endl;
b.age = 21;
cout << b.name << ", age " << b.age << endl << endl;
student c("Miki", 45);
cout << c.name << ", age " << c.age << endl << endl;
cout << "Bye!" << endl << endl;
return 0;
}
运行结果
Hello!
构造函数,申请了100个char元素的动态空间
no name, age 0
构造函数,申请了100个char元素的动态空间
John, age 0
John, age 21
构造函数,申请了100个char元素的动态空间
Miki, age 45
Bye!
析构函数,释放了100个char元素的动态空间
析构函数,释放了100个char元素的动态空间
析构函数,释放了100个char元素的动态空间
15 访问类的接口
前面定义类使用的是关键字struct,而从这里开始,将使用关键字class定义类。struct关键字定义的类的内部成员默认都是公有的(public),谁都可以进行访问。而class关键字定义的类的内部成员默认都是私有的(private),外部函数无法直接访问。为了保证程序的安全性(以免被外部使用者随意更改),使用class关键字进行类的定义,显然比sturct更好。同时class也有三个访问限定符,用于限定其“:”后面的诸多成员是否能够在类的外面被直接访问,即public(公有)、private(私有)、protected(保护)。这里将介绍将内部成员变量设置为私有时,外部如何通过编写的函数,访问这些内部私有成员变量。
例程1:class关键字定义的类的内部成员默认是私有的
#include <iostream>
#include <cstring>
using namespace std;
// 使用class关键字定义类
class student {
char *name;
int age;
student(const char *n = "no name", int a = 0) {
name = new char[100]; // 比malloc好!
strcpy(name, n);
age = a;
cout << "构造函数,申请了100个char元素的动态空间" << endl;
}
virtual ~student() { // 析构函数
delete name; // 不能用free!
cout << "析构函数,释放了100个char元素的动态空间" << endl;
}
};
int main() { // 如果将前面的 class关键字更改为struct,则不会报错
cout << "Hello!" << endl << endl;
student a; //编译出错:无法访问 private 成员(在“student”类中声明)
cout << a.name << ", age " << a.age << endl << endl; //编译出错
student b("John"); //编译出错
cout << b.name << ", age " << b.age << endl << endl;//编译出错
b.age = 21; //编译出错
cout << b.name << ", age " << b.age << endl << endl;//编译出错
return 0;
}
例程2:一般将类中的函数接口公开,成员变量设置为私有
这样外部就不能直接访问类的私有成员变量,但是可以间接访问,方法见下一个例程。类的对象自己是可以访问私有成员变量的。public的公开成员(一般是成员函数)称为这个类的对外接口,外部函数只能通过这些接口访问类的对象。private等非public的成员变量包含内部细节,不对外公开,从而可以封装保护类的对象!
#include <iostream>
#include <cstring>
using namespace std;
class student {
//默认私有的成员变量,等价于 private:
char *name;
int age;
public: //公开的 成员函数
student(const char *n = "no name", int a = 0) {
name = new char[100]; // 比malloc好!
strcpy(name, n);
age = a;
cout << "构造函数,申请了100个char元素的动态空间" << endl;
}
virtual ~student() { // 析构函数
delete name; // 不能用free!
cout << "析构函数,释放了100个char元素的动态空间" << endl;
}
};
int main() {
cout << "Hello!" << endl << endl;
student a; //OK 因为构造函数定义于 public
cout << a.name << ", age " << a.age << endl ; //编译出错: 无法访问 private 成员(在“student”类中声明)
student b("John"); // OK
cout << b.name << ", age " << b.age << endl ;//编译出错
b.age = 21;
cout << b.name << ", age " << b.age << endl ;//编译出错
return 0;
}
例程3:编写公开(public)的用于访问内部变量(private)的成员函数
编写指定的函数,用于获取或修改内部变量的数据,同时将该函数设置为public
#include <iostream>
#include <cstring>
using namespace std;
class student {
//默认私有的,等价于 private:
char *name;
int age;
public: //公开的
// 编写用于获取/修改内部变量name age的函数
char *get_name() { return name; }
int get_age() { return age; }
void set_age(int ag) { age = ag; }
student(const char *n = "no name", int a = 0) {
name = new char[100]; // 比malloc好!
strcpy(name, n);
age = a;
cout << "构造函数,申请了100个char元素的动态空间" << endl;
}
virtual ~student() { // 析构函数
delete name; // 不能用free!
cout << "析构函数,释放了100个char元素的动态空间" << endl;
}
};
int main() {
cout << "Hello!" << endl << endl;
student a;
cout << a.get_name() << ", age " << a.get_age() << endl ;
student b("John");
cout << b.get_name() << ", age " << b.get_age() << endl ;
b.set_age(21);
cout << b.get_name() << ", age " << b.get_age() << endl ;
return 0;
}
例程4:定义一个数组类 Array
#include <iostream>
#include <cstdlib>
using namespace std;
class Array {
int size;
double *data;
public:
Array(int s) {
size = s;
data = new double[s];
}
virtual ~Array() {
delete[] data;
}
// 对类 array 进行运算符 [] 的重载
double &operator [] (int i) {
if (i < 0 || i >= size) {
// cerr是C++标准错误输出流
// Cerr通常用于输出错误信息与其他不属于正常逻辑的输出内容。
cerr << endl << "Out of bounds" << endl;
throw "Out of bounds"; // 抛出异常 直接一个throw也可以
}
else return data[i]; // 一切正常,则返回 data[i]
}
};
int main() {
Array t(5);
t[0] = 45; // OK t.data[0] = 45
t[4] = t[0] + 6; // OK
cout << t[4] << endl; // OK
t[10] = 7; // error!
return 0;
}
16 拷贝构造函数
拷贝构造函数:定义一个类对象时,用同类型的另外一个对象初始化该对象。(类似于 int b=12; int c=b;)
赋值运算符:一个对象赋值给另外一个对象
例程1:使用默认的 strcpy、= 拷贝对象会报错
#define _CRT_SECURE_NO_WARNINGS //windows系统
#include <iostream>
#include <cstring>
using namespace std;
struct student {
char *name;
int age;
// 带默认参数的构造函数
student(const char *n = "no name", int a = 0) {
name = new char[100];
strcpy(name, n); // 将n拷贝给name
age = a;
cout << "构造函数,申请了100个char元素的动态空间" << endl;
}
virtual ~student() { // 析构函数
delete[] name;
cout << "析构函数,释放了100个char元素的动态空间" << endl;
}
};
int main() {
student s;
student k("John", 56);
cout << k.name << ", age " << k.age << endl;
// 下面注释的内容,编译时会出错。
//student m(s); //拷贝构造函数
//s = k; //赋值运算符
//cout << s.name << ", age " << s.age << endl;
return 0;
}
默认的拷贝是“硬拷贝”或“逐成员拷贝”,name指针指向同一块字符数组。当被拷贝的对象s被释放时,对象m拷贝的name指向的内存随之被释放掉了。所以执行到对象m的析构函数时,name指向的内存已经没有可以释放的了,就会报错。
为此,需要编写拷贝构造函数,保证其各自有单独的动态数组空间,见例程2。
vscode中编译报错,指向 delete[] name;
例程2:编写拷贝构造函数,进行逐个元素的拷贝
编写拷贝构造函数,保证其各自有单独的动态数组空间
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
using namespace std;
struct student {
char *name;
int age;
student(const char *n = "no name", int a = 0) {
name = new char[100];
strcpy(name, n);
age = a;
cout << "构造函数,申请了100个char元素的动态空间" << endl;
}
student(const student &s) { // 拷贝构造函数 Copy constructor
name = new char[100];
strcpy(name, s.name); // 单独进行拷贝
age = s.age;
cout << "拷贝构造函数,保证name指向的是自己单独的内存块" << endl;
}
student & operator=(const student &s) { // 重载拷贝构造运算符
strcpy(name, s.name);
age = s.age;
cout << "拷贝构造函数,保证name指向的是自己单独的内存块" << endl;
return *this; //返回 “自引用”
}
virtual ~student(){ // 析构函数
delete[] name; // 不能用free!
cout << "析构函数,释放了100个char元素的动态空间" << endl;
}
};
int main() {
student s;
student k("John", 56);
cout << k.name << ", age " << k.age << endl ;
student m(k); // 现在不会报错了
s = k; // s = operator=(k)
cout << s.name << ", age " << s.age << endl ;
return 0;
}
17 类模板template
与前面定义函数模板类似,我们也可以定义类模板,依旧使用关键字template。在实例化(定义)类的对象时,可以指定 T属于何种数据类型。这里同样也可以如前面的函数模板一样,定义多个通用数据类型。
#include <iostream>
#include <cstdlib>
using namespace std;
// 定义一个数组类模板,T为通用数据类型
// 在实例化(定义)类的对象时,可以指定 T属于何种数据类型
// 如:Array<data_type> test(6);
template<class T>
class Array {
T size;
T *data;
public:
Array(int s) {
size = s;
data = new T[s];
}
virtual ~Array() {
delete[] data;
}
// 返回值为类型 T 的运算符重载函数
T &operator [] (int i) {
if (i < 0 || i >= size) {
// cerr是C++标准错误输出流
// Cerr通常用于输出错误信息与其他不属于正常逻辑的输出内容。
cerr << endl << "Out of bounds" << endl;
throw "index out of range";
}
else return data[i];
}
};
int main() {
Array<int> t(5); // 取T为 int型
t[0] = 45; // OK
t[4] = t[0] + 6; // OK
cout << t[4] << endl; // OK
//t[10] = 7; // error!
Array<double> a(5); // 取T为 double型
a[0] = 45.5; // OK
a[4] = a[0] + 6.5; // OK
cout << a[4] << endl; // OK
a[10] = 7.5; // error!
return 0;
}
18 容器 container
所谓容器,即是将最常运用的一些数据结构(data structures)用类模板实现出来,用于容纳特定类型的对象。使用容器时,我们无需预先告诉他需要存储多少个数据,我们只需定义一个容器对象,然后调用该容器提供的方法即可,容器将会自动完成内存的申请与释放。
根据数据在容器中的排列特性,容器可分为序列式和关联式两种,包括vector、deque、list、set、map等。我们后面只会介绍序列式容器vector。
19 迭代器 iterator
后面的几个知识点中,将提及到“迭代器”,迭代器是一种检查容器内元素并遍历元素的数据类型,它提供类似指针的功能,用于对容器的内容进行走访。迭代器类似于指针类型,他也提供了对对象的间接访问。很多数据类型(容器)都在其内部定义了自己的迭代器,而我们便可以通过调用已经定义好的迭代器,访问该种数据类型(容器)的对象中的数据。可以理解为,定义某种数据类型(容器)的同时,也定义了用于访问该数据类型(容器)中的数据的功能函数。
大部分的容器都会提供成员函数begin()和end(),分别用于获取开始位置和结束位置的迭代器。需注意,end()将获取末位数据的后一位的地址。
// 将逐行输出 arr 字符串里面的所有元素
// std::string 里面也有迭代器可供使用
#include <iostream>
#include <string>
using namespace std;
int main()
{
std::string arr = "boy next door";
string::iterator ii; // 定义一个迭代器
for(ii = arr.begin(); ii!= arr.end(); ii++)
{ // ii 保存arr中元素的地址, *ii则指向了地址保存的对象
cout << *a << endl; // 将逐行输出 arr 字符串里面的所有元素
}
// 像下面这样书写也 ok
// 这里的 auto 将自动确定变量a的数据类型,即 string::iterator
// 使用迭代器函数 .begin() .end()
for(auto a = arr.begin(); a!= arr.end(); a++)
{ // a 保存arr中元素的地址, *a则指向了地址保存的对象
cout << *a << endl; // 将逐行输出 arr 字符串里面的所有元素
}
return 0;
}
根据该数据类型中,内部保存数据的结构,其提供的迭代器可分为不同的种类,而这些不同种类的迭代器,其可以实现的功能也不相同,如下图。(暂时不用深究,目前有个概念即可。)
后面将使用到std::string里面的迭代器,该迭代器为 随机访问迭代器。
20 string类
string是C+ +标准库的一个重要的部分,主要用于字符串处理。C++的算法库对string类有着很好的支持,并且string类还和c语言的字符串之间有着良好的接口。string类里面编写了前面讲解过的各类函数,构造函数、运算符重载、拷贝构造函数… 后面的内容则是使用这些内部编写的函数。
这里关于string类的使用介绍并不全面,更多内容还得百度百度。
例程1: string 对象的初始化
//string对象的初始化
#include <iostream>
#include <string> //typedef std::basic_string<char> string;
using namespace std;
// typedef 给指定的数据类型取别名
// 如: typedef int AA; // AA 即代表整型数据类型
typedef string String;
int main() {
// string类里面编写了前面讲解过的各类函数
// 构造函数、运算符重载、拷贝构造函数...
string s1; //默认构造函数:没有参数或参数有默认值
String s2("hello"); //普通构造函数 String就是string
s1 = "Anatoliy"; // 赋值运算符(运算符重载)
String s3(s1); //拷贝构造函数 string s3 =s1;
cout << "s1 is: " << s1 << endl;
cout << "s2 is: " << s2 << endl;
cout << "s3 is: " << s2 << endl;
// 一些字符串截取操作演示
// 1 - C++ string
// 2 - 字符串截取长度
string s4("this is a C_sting", 10);
cout << "s4 is: " << s4 << endl;
// 1 - C++ string
// 2 - 起始位置 start position
// 3 - 字符串截取长度 number of characters
string s5(s4, 6, 4); // copy word from s4
cout << "s5 is: " << s5 << endl;
// 1 - 字符串长度 number characters
// 2 - 字符 character itself
string s6(15, '*');
cout << "s6 is: " << s6 << endl;
// 1 - 字符串起始 start iterator
// 2 - 字符串结尾 end iterator
string s7(s4.begin(), s4.end() - 5);
cout << "s7 is: " << s7 << endl;
// you can instantiate string with assignment
string s8 = "Anatoliy";
cout << "s8 is: " << s8 << endl;
// 字符串拼接 使用到了运算符+的重载
string s9 = s1 + "hello"+ s2; //s1 + "hello"+ s2的结果是string类型的对象(变量)
cout << "s9 is: " << s9 << endl;
return 0;
}
运行结果:
s1 is: Anatoliy
s2 is: hello
s3 is: Anatoliy
s4 is: this is a
s5 is: s a
s6 is: ***************
s7 is: this
s8 is: Anatoliy
s9 is: Anatoliyhellohello
例程2:访问、遍历string中的元素。
const_iterator 为常量迭代器,其只能访问,不能修改数据;而iterator则可以修改数据。
#include <iostream>
#include <string>
using namespace std;
int main() {
string s = "hell";
string w = "worl!";
s = s + w; //s +=w;
// 0) 不使用迭代器
for (int ii = 0; ii != s.size(); ii++)
cout << ii << " " << s[ii] << endl;
cout << endl;
// 1) const_iterator 常量迭代器 (不可修改数据)
string::const_iterator cii;
int ii = 0;
for (cii = s.begin(); cii != s.end(); cii++)
cout << ii++ << " " << *cii << endl;
cout << endl;
// 2) iterator 迭代器 可以修改数据
string::iterator cjj; // 可以把这里改成const_iterator,看看会不会报错
ii = 0;
for (cjj = s.begin(); cjj != s.end(); cjj++)
{
*cjj = '1'; // 这里将所有元素修改为 1
cout << ii++ << " " << *cjj << endl;
}
return 0;
}
21 vector类
vector类是C+ +标准库的一个重要的部分,中文偶尔译作“容器”,它是一个多功能的,能够操作多种数据结构和算法的模板类和函数库。Vector之所以被认为是一个容器,是因为它能够像容器一样存放各种类型的对象,简单地说,vector是一个能够存放任意类型的动态数组,能够增加和压缩数据。
前面介绍容器时提到:所谓容器,即是将最常运用的一些数据结构(data structures)用类模板实现出来,用于容纳特定类型的对象。使用容器时,我们无需预先告诉他需要存储多少个数据,我们只需定义一个容器对象,然后调用该容器提供的方法即可,容器将会自动完成内存的申请与释放。
所以,vector其实是一个类模板,我们在使用时需要指定其变量类型。
// 格式如下
vector<数据类型> 对象名
vector也有很多的函数接口,感兴趣可以百度,这里只介绍部分内容。
例程1:存储多个学生的成绩数据
迭代器,在前面已经讲过啦
#include <vector> // 包含 vector
#include <iostream>
using namespace std;
int main() {
// 定义一个double型的vector对象 student_marks 用于存储学生分数
vector<double> student_marks;
int num_students;
// 输入学生的数量
cout << "Number of students: " << endl;
cin >> num_students;
// .resize 设置容器student_marks的内存大小
student_marks.resize(num_students);
// 提示用户输入 num_students 个学生的成绩
for (vector<double>::size_type i = 0; i < num_students; i++) {
// 这里的 size_type 是vector类中定义的类型,为unsigned类型
// string类中也有定义
cout << "Enter marks for student #" << i + 1 << ": " << endl;
cin >> student_marks[i];
}
cout << endl;
// iterator 迭代器
// .begin() 指向容器中第一个元素
// .end() 指向容器中最后一个元素
// 下面的for循环遍历了容器中的每个元素,并输出
for (vector<double>::iterator it = student_marks.begin();
it != student_marks.end(); it++) {
cout << *it << endl;
}
// 上面的内容,也可使用 auto 实现(自动获取变量的数据类型)
cout << "Use auto:" << endl;
for (auto its = student_marks.begin();
its != student_marks.end(); its++) {
cout << *its << endl;
}
return 0;
}
例程2:vector的其他内容补充
#include <vector> // 包含 vector
#include <iostream>
using namespace std;
int main() {
// 使用vector创建数组的多种形式
vector<int> v(10,2); // 创建一个含有10个元素的int数组,且元素值皆为2
vector<int> v1(10); // 创建一个含有10个元素的int数组,且元素值皆为0 (默认为0)
// 使用迭代器,遍历输出数组中的元素
for(auto p=v.begin();p!=v.end();p++)
cout << *p << ' ' ;
cout << endl;
// 使用.push_back 在vector对象v的末尾新增一个元素
v.push_back(666);
cout << "Add a number"<< endl ;
for(auto p=v.begin(); p!=v.end(); p++)
cout << *p << ' ' ;
cout << endl;
return 0;
}
22 继承Inheritance
继承 (inheritance) 就是在一个已存在的类的基础上建立一个新的类。一个新类从已有的类获得其已有特性, 称为类的继承。从已有的类 (父类) 产生一个新的子类, 称为类的派生。
●已存在的类: 基类 (base class) 或父类 (father class)
●新建立的类: 派生类 (derived class) 或子类 (son class)
从1个或多个父类(parent class) / 基类(base class)继承,即继承父类的属性和行为,但也有自己的特有属性和行为。
例程1:这里以雇员和经理进行举例
#include <iostream>
#include <string>
using namespace std;
// 雇员 姓名
class Employee{
string name;
public:
Employee(string n);
void print();
};
// 经理,在雇员的基础上建立(继承了Employee的public内容)
class Manager: public Employee{
int level; // 比雇员多了一个 等级
public:
Manager(string n, int l = 1);
//void print();
};
// Employee 的构造函数(初始化)
// 因为name是string类型,所以可以直接使用name(xx)进行赋值
Employee::Employee(string n) :name(n)//初始化成员列表
{// 等效于 name = n
}
void Employee::print() {
cout << name << endl;
}
// Manager 的构造函数(初始化)
Manager::Manager(string n, int l) :Employee(n), level(l) {
}
// 派生类的构造函数只能描述它自己的成员和其直接基类的初始式,不能去初始化基类的成员。
// 下面的形式是错误的 不能使用name(n)对n进行初始化,需要借助其直接基类进行初始化。(Employee(n))
//Manager::Manager(string n, int l) : name(n), level(l) {
//}
int main() {
Manager m("Zhang",2);
Employee e("Li");
m.print();
e.print();
return 0;
}
23 多重继承
我们可以从一个类中派生出多个不同的类。
class Employee{ // 基类
public:
virtual void print();
};
class Manager : public Employee{ // 子类
public:
void print();
};
class Secretary : public Employee{ // 子类
public:
void print();
};
也可以从多个不同的类,派生出一个类,即多重派生(Multiple inheritance)。
class One{
// class internals
};
class Two{
// class internals
};
// 同时继承One和Two两个类。 使用“,”分隔开
class MultipleInheritance : public One, public Two
{
// class internals
};
24 虚函数Virtual
简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。虚函数的作用就是实现多态(Polymorphism,专业术语)。也可说虚函数的作用为实现子类函数重写父类函数(看不懂不要紧,先看下面的例程)。
派生类(子类)的指针可以自动转化为基类指针。下面的例程1中,即是子类Manager的指针(&m)可以自动转换为基类Employee的指针。
例程1:虚函数的效果演示
用一个指向基类的指针分别指向基类对象和派生类对象,并2次调用print()函数输出,结果如何?
#include <iostream>
#include <string>
using namespace std;
//将print声明为虚函数Virtual Functions
class Employee{ // 基类
string name;
public:
Employee(string n);
virtual void print(); // 声明为虚函数
//void print(); // 不声明为虚函数
// 【不将print声明为虚函数时】,程序输出结果不同,可以试试。
};
class Manager : public Employee
{
int level;
public:
Manager(string n, int l = 1);
void print(); // 基类声明为了虚函数,子类就可以不用声明了
// 当然,如果有一个类继承了类Manager,那这里可以声明为虚函数
};
Employee::Employee(string n) :name(n) {
}
void Employee::print() { // 基类的print函数
cout << name << endl;
}
Manager::Manager(string n, int l) :Employee(n), level(l) {
}
void Manager::print() { // 子类的print函数
cout << level << "\t";
Employee::print();
}
int main() {
Employee *p; // 定义了 employee类型的指针 (基类指针)
Manager m("Zhang", 1); // 子类对象
Employee e("Li"); // 基类对象
p = &e; // 指针指向 基类对象
p->print();
p = &m; // 指针指向 子类对象
// 子类Manager的指针(&m)可以自动转换为基类Employee的指针。
p->print();
}
上述例程中,如果基类中的print()声明为虚函数(加上virtual关键字),则main()函数中的两个print函数,将会分别调用基类的print和子类的pirnt,即会根据实际指向的对象的类型调用对应的print函数。
如果,基类中的print()函数不是虚函数(不加virtual关键字),则main函数中的两个print函数,都会调用指针类型Employee中定义的print函数,即直接调用指针类型对应的类中定义的print函数。
两种情况下,上述例程输出的结果如下:
// 基类Employee中的print仅打印 name
// 子类Manager中的print将打印 level name
// 基类中,print() 声明为虚函数
Li
2 Zhang
// 基类中,print() 非虚函数
Li
Zhang
多态,指为不同数据类型的实体提供统一的接口。当程序运行时,相同的消息可能会发送给多个不同类型的对象,而系统则可以根据对象所属类别,引发对应类别的方法,从而体现出不同的行为状态。上述这种将基类中的print声明为虚函数后,main()函数中根据实际指向的对象的类型调用对应的print函数的现象,就可以称之为多态。
上面的例程1中,我们可以大致的了解到多态是个啥,而下面的例程2则将加强对虚函数实现多态的理解。
例程2:如何使用一个数组保存雇员和经理的信息?在看具体的程序之前,可以先看看程序运行时的效果,如下:
// 前面我们通过例程1了解到,派生类的指针可以自动转化为基类指针。
// 因此,我们可以将所有雇员保存于一个Employee* employees[100];
// ps:经理本质上也是公司的雇员,只是等级比较高罢了。
#include <iostream>
#include <string>
using namespace std;
// 前面的内容和例程1完全一样,可以直接从main函数开始看
class Employee{
string name;
public:
Employee(string n);
virtual void print(); // 将print声明为虚函数
};
class Manager : public Employee
{
int level;
public:
Manager(string n, int l = 1);
void print();
};
Employee::Employee(string n) :name(n) {
}
void Employee::print() {
cout << name << endl;
}
Manager::Manager(string n, int l) :Employee(n), level(l) {
}
void Manager::print() {
cout << level << "\t";
Employee::print();
}
int main() {
// 定义基类Employee类型的指针数组
// 用于存放 Employee和 Manager 数据
Employee* employees[100];
int e_num = 0; // 记录实际数据数目
Employee* p; // 基类Employee类型的指针
string name; int level;
char cmd; // 根据输入的指令,执行不同的程序逻辑
while (cin >> cmd) {
if (cmd == 'M' || cmd == 'm') { // 新增Manager数据
cout << "请输入姓名和级别" << endl;
cin >> name >> level;
p = new Manager(name, level);
// 将Manager数据保存到指针数组中
employees[e_num] = p; e_num++;
}
else if (cmd == 'e' || cmd == 'E') { // 新增Employee
cout << "请输入姓名" << endl;
cin >> name;
p = new Employee(name);
// 将Employee数据保存到指针数组中
employees[e_num] = p; e_num++;
}
else break; // 输入其他命令,直接退出while循环
cout << "请输入命令" << endl;
}
// 打印出 指针数组employees中的所有对象
for (int i = 0; i < e_num; i++) {
employees[i]->print();
// 这里会 根据实际指向的对象的类型调用对应的print函数
}
return 0;
}
25 纯虚函数和抽象类
纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。纯虚函数也可以叫抽象函数,一般来说它只有函数名、参数和返回值类型,不需要函数体。这意味着它没有函数的实现,需要让派生类去实现。
纯虚函数(pure virtual function )和抽象类(abstract base class)函数体=0的虚函数称为“纯虚函数”,包含纯虚函数的类称为“抽象类”,抽象类不能直接实例化。而派生类要想实例化,需要对纯虚函数进行函数实现,不然对派生类进行实例化也会报错的。
例程1:抽象类不能实例化
#include <string>
class Animal // 抽象类:因为其声明了纯虚函数
{
protected:
std::string m_name;
public:
Animal(std::string name): m_name(name){ }
std::string getName() { return m_name; }
virtual const char* speak() = 0; // 纯虚函数
};
int main() {
Animal a; //错:抽象类不能实例化(不能定义抽象类的对象(变量))
}
例程2:从抽象类派生的类如果没有实现所有的纯虚函数,则仍然是“抽象类”,不能实例化
#include <iostream>
class Animal // 抽象类
{
protected:
std::string m_name;
public:
Animal(std::string name): m_name(name){ }
std::string getName() { return m_name; }
virtual const char* speak() = 0; // 纯虚函数
};
class Cow : public Animal // 依旧为抽象类
{
public:
Cow(std::string name): Animal(name){}
// We forgot to redefine speak
};
int main(){
Cow cow("Betsy"); //仍然错:因为Cow仍然是抽象类
std::cout << cow.getName() << " says " << cow.speak() << '\n';
}
例程3:像下面这样实现了所有的纯虚函数就没问题了,Cow将不再是一个抽象类。
#include <iostream>
class Animal // 抽象类
{
protected:
std::string m_name;
public:
Animal(std::string name): m_name(name){ }
std::string getName() { return m_name; }
virtual const char* speak() = 0; // 纯虚函数
};
class Cow : public Animal // 可以进行实例化的类
{
public:
Cow(std::string name) : Animal(name) { }
// 这里对继承的纯虚函数进行了函数实现
virtual const char* speak() { return "Moo"; }
};
int main()
{ // 不再报错
Cow cow("Betsy");
std::cout << cow.getName() << " says " << cow.speak() << '\n';
}