【c/c++】从C到C++快速入门(会c语言想学c++看这篇文章就够了)

背景

从事嵌入式多年,一直和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!

构造函数,申请了100char元素的动态空间
no name, age 0

构造函数,申请了100char元素的动态空间
John, age 0

John, age 21

构造函数,申请了100char元素的动态空间
Miki, age 45

Bye!

析构函数,释放了100char元素的动态空间
析构函数,释放了100char元素的动态空间
析构函数,释放了100char元素的动态空间

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';
}

请添加图片描述

  • 22
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

莲花码农

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

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

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

打赏作者

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

抵扣说明:

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

余额充值