C++学习之旅


记录一下自己的C++学习内容,持续更新ing~

一、初识C++

注释、变量、常量
//头文件
#include 

//声明命名空间
using namespace std;
/*
main是一个程序的入口,有且仅有一个
*/

//常量  1、#define宏常量  2、const修饰变量
#define Day = 7;

int main()
{
    //输出
    cout << "hello world!" << endl;
    
    //变量,方便管理内存空间
    //数据类型 变量名 = 初始值
    int a = 10;
    
    //用const修饰变量也是常量
    const int month = 12;
    
    
    //保持命令行窗口
    system("pause");
    return 0;
}

1、C++中的关键字

关键字

2、标识符(变量、常量)命名规则

  • 标识符不能是关键字
  • 标识符只能由字母数字下划线组成
  • 第一个字符必须为字母或下划线
  • 标识符中区分大小写

二、数据类型

1、整形

数据类型 占用空间
short(短整型) 2字节
int(整形) 4字节
long(长整型) win4字节linux4字节(32位)8字节(64位)
long long(长长整形) 8字节

2、sizeof()关键字

int a = 10;
cout << sizeof(a) << endl;//打印对象所占内存空间字节数

3、实型(浮点型)

数据类型 占用空间 有效数字范围(统计小数点前后的)
float 4字节 7位
double 8字节 15~16位

默认情况下输出一个小数会显示出6位有效数字,要增加需要特殊配置

科学计数法
float f1 = 3e2;// 3*10^2结果为300
float f2 = 3e-2;// 3*0.1^2结果为0.03

4、字符型

  • C和C++中字符型变量只占用==1个字节==
  • 字符在内存中存放的是对应的ASCII编码
  • 创建字符变量要用==单引号==
  • 字符型变量单引号内只能放==一个字符(单个字母)==
//查看ASCII码
char ch = 'a';
cout << (int)ch << endl;
//输出97
//也可以用ASCII给字符变量赋值
char cha =  65;
cout << cha << endl;

5、转义字符

常用的转义字符\n \\ \t

\n 换行,将位置移到下一行开头

\\ 代表一个反斜线字符\

\t 水平制表(跳到下一个TAB位置)可以整齐的输出后面的内容,占8个字符

    cout << "abcdefg\t前面最多8个字符" << endl;
    cout << "ab\t前面最多8个字符" << endl;
    cout << "abc\t前面最多8个字符" << endl;
    cout << "abcde\t前面最多8个字符" << endl;
//如果满了会重新开辟8个位置
    cout << "abcdefgh\t前面最多8个字符" << endl;
//结果:
    //abcdefg 前面最多8个字符
    //ab      前面最多8个字符
    //abc     前面最多8个字符
    //abcde   前面最多8个字符
    //abcdefgh        前面最多8个字符

6、字符串型

1、C风格字符串char 变量名[] = ”字符串值“

2、C++风格字符串string 变量名 = “字符串值”

  • 使用string类型字符串,要加入#include <string>头文件

7、布尔类型

bool类型占用1个字节

bool flag = true; //  1
flag = false; // 0

8、字符输入

cin >> 变量

三、运算符

算数运算符、赋值运算符、比较运算符、逻辑运算符

1、算术运算符

  • / 除 1、除数不能为0 2、整型相除不会保留小数部分

  • % 取余 1、除数为0不能做取模 2、两个小数不能做取余

  • ++/-- 先自加再计算表达式 a=2;b=++a; a=3;b=3;

  • ++/-- 先计算表达式再自加 a=2;b=a++; a=3;b=2;

2、赋值运算符

  • a+=2;就等价于a=a+2;
  • a%=2;就等价于a=a%2;

同理+-*/

3、比较运算符

相等: ==

不相等: !=

大于> 小于< 大于等于>= 小于等于<=

4、逻辑运算符

!非

&&与

||或

5、程序流程结构

C/C++三种运行结构:顺序结构、选择结构、循环结构

1、三目运算符

表达式1 ?表达式2 :表达式3

等价于:

if 表达式1

{表达式2}

else {表达式3}

2、switch语句

switch (整型或者字符型)//不能像if一样判断一个区间
    {
    case 结果1:表达式; break;
    case 结果2:表达式; break;
    case 结果3:表达式; break;
    //...
    default:表达式; break;
    }

while(…){…}

do{…}while(…);

#include 
#include 
using namespace std;
int main()
{
    int num = 100;
    do{
        //三位数内的所有水仙花数
        int a= 0;
        int b= 0;
        int c= 0;
        a = num %10;//获取个位
        b = num / 10 % 10;//获取十位
        c = num / 100;//获取百位
        int res = pow(a,3)+pow(b,3)+pow(c,3);
        if(num == res)//判断是水仙花数才输出
        {
            cout << "水仙花数有:" << num << endl;
          }
        num++;
    }
    while (num < 1000);
    system("pause");
    return 0;
}

for(起始表达式;条件表达式;末尾循环体){…}

for (int i= 1;i<10;i++){ //输出九九乘法表       
        for (int j = 1;j<=i;j++){
            cout << j << "*" << i << "=" << i*j << "\t";
        }
        cout << " " << endl;
    }

3、跳转语句

break;continue;goto;

//代码...,会执行
goto FLAG;//在程序中不建议使用goto,了解即可,会导致程序混乱
//代码...中间的都不会执行
FLAG:
//代码...会执行

四、数组

1、概念

  • 数组内的元素都是相同的数据类型
  • 数组由连续的内存位置组成

2、一维数组

定于数组:

1.数据类型 数组名[数组长度];

2.数据类型 数组名[ 数组长度 ] = {值1,值2....}

3.数据类型 数组名[ ] = { 值1,值2...}

  • 定义数组时必须要有初始长度
  • 数组中没有被定义的位置,为0
  • 数组下标从0开始索引
int arr[] = {8,9};
cout << (int)arr << endl;//arr的首地址
cout << (int)&arr[0] << endl;//arr第一个元素的地址
int arr[] = {8,0,1,9,3,6,5,123,122,45,22,4};//冒泡排序,任意数组大小都可以排序
for (int i=0;iarr[j+1]){
            int tmp = arr[j];
            arr[j] = arr[j+1];
            arr[j+1] = tmp;
        }
    } 
    continue;
}
for (int item =0 ;item < sizeof(arr) / sizeof(arr[0]);item++)
{
    cout << arr[item] << " " ;
    cout << endl ;
}

3、二维数组

定义方式:

1.数据类型 数组名[ 行数 ][ 列数 ];

2.数据类型 数组名[ 行数 ][ 列数 ] = {{数据1,数据2},{数据3,数据4}};建议使用第二种,更加直观

3.数据类型 数组名[ 行数 ][ 列数 ] = {数据1,数据2,数据3,数据4};

4.数据类型 数组名[ ][ 列数 ] = {数据1,数据2,数据3,数据4};

int arr[3][4] = {{1,2,3},{4,5,6}};
cout << sizeof(arr) << endl;//二维数组占用的内存空间48
cout << sizeof(arr[0][0]) << endl;//二维数组第一个元素占用的内存空间4
cout << (long long)arr << endl;//二维数组内存首地址
cout << (long long)&arr[0] << endl;//二维数组第一行的首地址
cout << (long long)&arr[0][0] << endl;//二维数组第一个元素的地址
cout << sizeof(arr)/sizeof(arr[0]) << endl;//获取二维数组的行数
cout << sizeof(arr[0])/sizeof(arr[0][0]) << endl;//获取二维数组列数

五、函数

1、定义

返回值类型  函数名(参数列表)
{
    函数体语句
    return 表达式;
}

2、值传递

#include 
using namespace std;

void swap(int num1,int num2){
    int tmp = num1;
    num1 = num2;
    num2 = tmp;
    return;
}
int main()
{
    int a= 2;
    int b= 3;
    swap(a,b);
    cout << a << " " << b << endl;//结果依旧是2 3
    //在值传递的时候,函数体内的形参的变化,不会影响实参   
    system("pause");
    return 0;
}

3、函数样式

  • 无参无返
  • 有参无返
  • 无参有返
  • 有参有返

4、函数声明

函数类型 函数名(形参列表);此时函数可以写在最后

声明可以写多次,但是函数定义只能有一次

5、函数分文件编写

1、创建.h的头文件

2、创建.cpp的源文件

3、在头文件写函数声明

4、在源文件写函数定义

例:

头文件(.h):

#include //记得添加系统头文件

using namespace std;

void swap(int num1,int num2);//函数声明

源文件(.cpp):

//记得引用写好函数声明的头文件
#include "cpp_obj.h"//注意自己写的头文件要用双引号

void swap(int num1,int num2){
    int tmp = num2;
    num2 = num1;
    num1 = tmp;
    cout << "num1 =" << num1 << endl;
    cout << "num2 =" << num2 << endl;
}

使用函数的主文件:

#include “cpp_obj.h”//需要添加头文件使用

六、指针

指针就是一个地址

定义:数据类型 * 指针变量名;

使用:

//可以通过解引用(在指针前加 * )的方式找到指针指向的内存
*p = 2;
cout << a << endl; //结果为2,数据被修改
cout << *p << endl;  //结果为2

内存地址:

32位(无论什么类型)指针变量内存地址为4位;

32位(无论什么类型)指针变量内存地址为8位;

1、空指针

int* p = NULL;
  • 空指针用于给指针初始化
  • 内存编号0~255为系统占用,空指针是不可以被访问的

2、野指针

int* p = (int* )0x1100;//指针变量p指向内存编号为0x1100的空间
cout << *p << endl;//访问野指针会报错,0x1100内存并没有申请,没有访问权限,尽量避免出现野指针

空指针、野指针都不是我们申请的空间,因此请不要访问

3、const修饰指针

1.const修饰指针——常量指针

int a = 3;
const int * p = &a;//p就是一个常量指针
*p = 66;//不能更改
p = &b;//可以更改

指针的指向可以修改,但指针指向的值不可以修改;想要修改指针的指向,指向新的地址的内存必须和之前的值一致

2.const修饰常量——指针常量

int a = 3;
int * const p = &a;
*p = 66;//可以更改
p = &b;//不能更改

指针的指向不能更改,但是指针的值可以更改

3.const既修饰指针,也修饰变量

指针的指向和指针指向的值都不能修改

4.指针和数组

用指针访问数组中的元素

int a[4] = {1,2,3,4};
int * p = a;
cout << *p << endl;//结果为1
p++;
cout << *p << endl;//结果为2
cout << p[2] << endl; // 结果为3
p++;
cout << p[2] << endl; // 结果为4

for (int i=0;i < 4;i++){//用指针遍历数组
        cout << *p << endl;
        p++;}

5.指针和函数

函数定义的形参用指针代替,传入的值是实参的地址,实参内存地址被改变,值相应改变

通过指针代替形参传递,可以改变实参的值

#include 

using namespace std;

void bubble(int *arr, int len) // 如何传递数组:将首地址传进来
{
    for (int i = 0; i < 10 - 1; i++)
    {
        for (int j = 0; j < 10 - 1 - i; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

int main()
{
    // 封装一个函数,实现对一个数组的冒泡排序
    int array1[10] = {7, 2, 4, 5, 8, 10, 9, 6, 3, 1};
    int len = sizeof(array1) / sizeof(array1[0]);
    bubble(array1, len);
    int *p_array1 = array1;
    for (int i = 0; i < len; i++)
    {
        cout << *p_array1 << endl;
        p_array1++;
    }

    system("pause");
    return 0;
}

七、结构体

定义:struct 结构体名 { 结构体成员列表 };

#include 
#include 
using namespace std;

struct Stu{//结构体变量定义的时候不能省略
    string name ;
    int age;
    int score;
};

int main()
{
    struct Stu s1;
    s1.age = 12;
    s1.name = "张三";
    s1.score = 100;
    cout << s1.name << s1.age << s1.score << endl;
    Stu s2{ "李四", 19 , 99 };//结构体变量创建的时候可以省略
    cout << s2.name << s2.age << s2.score << endl;

    system("pause");
    return 0;
}

1、结构体数组

struct 结构体名 数组名[元素个数] = { { },{ },{ }, . . }

#include 
#include 
using namespace std;

struct Stu{//定义结构体
    string name ;
    int age;
    int score;
};

int main()
{
    struct Stu arr[3]=
    {
        { "李四", 19 , 99 },
        { "张三", 13 , 100 },
        { "李逵", 15 , 96 },
    };
    //使用结构体数组
    cout << arr[1].name << arr[1].age << arr[1].score << endl;

    system("pause");
    return 0;
}

2、结构体指针

#include 
#include 
using namespace std;

struct Stu
{ // 定义结构体
    string name;
    int age;
    int score;
};

int main()
{
    Stu stu1 = {"李逵", 15, 96}; // 创建结构体变量
    Stu *p = &stu1;              // 指针指向结构体变量
    p->age = 0;//结构体指针可以改变指向的内存
    cout << p->name << p->age << p->score << endl;//使用结构体指针

    system("pause");
    return 0;
}

3、结构体嵌套结构体

在结构体中可以定义另一个结构体作为成员,以便使用

#include 
#include 
using namespace std;

struct Info//嵌套的结构体
{
    int grade;   // 年级
    int classes; // 班级
    long long stu_num;//学号
};

struct Stu
{ // 定义结构体
    string name;
    int age;
    int score;
    struct Info stu1_info; // 学生的信息,需要提前定义
};

int main()
{
    Stu stu1 = {"李逵", 15, 96, 19, 1, 201902250101}; // 创建结构体变量
    Stu *p = &stu1;                                   // 指针指向结构体变量                                    // 结构体指针可以改变指向的内存
    cout << "学生姓名:" << p->name << endl;
    cout << "学生年龄:" << p->age << endl;
    cout << "学生成绩:" << p->score << endl;
    cout << "学生年级:" << p->stu1_info.grade << endl;
    cout << "学生班级:" << p->stu1_info.classes << endl;
    cout << "学生学号:" << p->stu1_info.stu_num << endl; // 使用结构体指针

    system("pause");
    return 0;
}

4、结构体做函数参数

值传递地址传递

#include 
#include 
using namespace std;

struct Stu
{ // 定义结构体
    string name;
    int age;
    int score;
};

void PrintfStu(struct Stu stu1){//值传递
    cout << "****打印学生信息(值传递版)****" << endl;
    cout << "学生姓名:" << stu1.name << endl;
    cout << "学生年龄:" << stu1.age << endl;
    cout << "学生成绩:" << stu1.score << endl;
}
int main()
{
    Stu stu1 = {"李逵", 15, 96}; // 创建结构体变量
    PrintfStu(stu1);

    system("pause");
    return 0;
}
#include 
#include 
using namespace std;

struct Stu
{ // 定义结构体
    string name;
    int age;
    int score;
};

void PrintfStu(struct Stu * p){//用指针接收地址
    cout << "****打印学生信息(指针版)****" << endl;
    cout << "学生姓名:" << p->name << endl;
    cout << "学生年龄:" << p->age << endl;
    cout << "学生成绩:" << p->score << endl;
    //函数体内部改变,可以改变实参的值
    //列:p->age = 19;则函数外部访问stu1.age结构为19
}
int main()
{
    Stu stu1 = {"李逵", 15, 96}; // 创建结构体变量
    //PrintfStu(stu1);//值传递
    PrintfStu(&stu1);//地址传递

    system("pause");
    return 0;
}

5、结构体中const的使用场景

#include 
#include 
using namespace std;

struct Stu
{ // 定义结构体
    string name;
    int age;
    int score;
};

void PrintfStu(const Stu * p){//用const修饰,限制指针的修改,防止函数体中误操作
    cout << "****打印学生信息(指针版)****" << endl;
    cout << "学生姓名:" << p->name << endl;
    cout << "学生年龄:" << p->age << endl;
    cout << "学生成绩:" << p->score << endl;
    //p->age=100; 操作失败,因为加了const修饰
}
int main()
{
    Stu stu1 = {"李逵", 15, 96}; // 创建结构体变量
    PrintfStu(&stu1);
    system("pause");
    return 0;
}

6、案例:结构体数组冒泡排序

#include 
#include 
using namespace std;

struct Hero
{
    string name;
    int age;
    string gender;
};

void Bubble_Hero(struct Hero hero_arr[5],int len){//冒泡排序函数
    for (int i = 0; i < len - 1; i++)
    {
        for (int j = 0; j < len - 1 - i; j++)
        {
            if (hero_arr[j].age > hero_arr[j + 1].age)
            {
                Hero tmp = hero_arr[j];
                hero_arr[j] = hero_arr[j + 1];
                hero_arr[j + 1] = tmp;
            }
        }
    }
}

void PrintHero(Hero *hero_p,int len)//打印函数
{
    for (int item = 0; item < len; item++)
    {
        cout << "年龄:" << hero_p->age << " "
             << "英雄:" << hero_p->name << endl;
        hero_p++;
    }
}

int main()
{
    Hero hero_arr[5] = {
        {"刘备", 23, "男"},
        {"关于", 22, "男"},
        {"赵云", 20, "男"},
        {"张飞", 21, "男"},
        {"貂蝉", 19, "女"},
    };
    int len = sizeof(hero_arr) / sizeof(hero_arr[0]);
    
    Bubble_Hero(hero_arr,len);//利用冒泡排序,将结构体数组排序打印
    PrintHero(hero_arr,len);

    system("pause");
    return 0;
}

八、面向对象

1、内存分区模型

  • 代码区:存放二进制代码,由操作系统管理
  • 全局区:存放全局变量和静态变量以及常量
  • 栈区:由编译器自动分配释放,存放函数的参数值,局部变量(在函数体内)等
  • 堆区:由程序员分配和释放,若程序员不是放,程序结束时由操作系统回收

意义:不用区域存放的数据,赋予不同的生命周期,给我们更大的灵活变成

1.1程序运行前

在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域

代码区:

存放CPU执行的机器指令(二进制)

代码区具共享的:共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可

代码区是只读的:使其只读的原因是防止程序意外地修改了它的指令

全局区:

全局变量静态变量(static)存放在此

全局区还包含了常量区字符串常量const修饰的全局常量也存放在此,该区域的数据在程序结束后由操作系统释放

int b = 12;//在函数体外就是全局变量
const int g_b = 12;//全局常量
int main()//主函数
{
    int a = 12;//在函数体内,是局部变量
    cout << "a(局部变量)的内存地址是:" << (long long)&a << endl;//a(局部变量)的内存地址是6487580
    const int l_a = 12;
    cout << "l_a(局部常量)的内存地址是:" << (long long)&l_a << endl;//l_a(局部常量)的内存地址是:6487576
    
    cout << "b(局部变量)的内存地址是:" << (long long)&b << endl;//b(全局变量)的内存地址是4206608
    cout << "g_b(全局常量)的内存地址是:" << (long long)&g_b << endl;//g_b(全局常量)的内存地址是:4210688
    static int c = 12;
    cout << "c(静态变量)的内存地址是:" << (long long)&c << endl;//c(静态变量)的内存地址是:4206612
    //常量,字符串都称为字符串常量
    cout << "字符串常量的内存地址是:" << (long long)&"字符串" << endl;//字符串常量的内存地址是:4210787
    
    
    system("pause");
    return 0;
}

1.2程序运行后

栈区:

由编译器自动分配释放,存放函数的参数值,局部变量等

注意事项:不要返回局部变量的地址,浅区开辟的数据由编译器自动释放

堆区:

由程序员分配释放,若程序员不释放程序结束时由操作系统回收

在C++中主要利用new在堆区开辟内存

1.3new操作符

C++中利用new操作符在堆区开辟数据

堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符delete

语法:new数据类型

利用new创建的数据,会返回该数据对应的类型的指针

int * Func(){
    int *p = new int(12);//new操作在堆区开辟了一块数据存放元素12,会返回该数据类型的指针
    //int *p = new int[11];//还可以new一个数组,返回的是数组的首地址
    return p;//返回指针地址
}
int main()
{
    int * p = Func();要用指针接收
    cout << *p << endl;//解析指针内存地址,输出
    delete p;//释放堆内存
    //cout << *p << endl;运行会报错,内存已经被释放,无法访问
}

C++中对象new出来和直接声明的区别

首先,最直观的,new出来的对象需要使用指针接收,而直接声明的不用。例如 A* a=new A() 与A a()。

new出来的对象是直接使用堆空间,而局部声明一个对象是放在栈中。

new出来的对象类似于申请空间,因此需要delete销毁,而直接声明的对象则在使用完直接销毁。

new出来的对象的生命周期是具有全局性,譬如在一个函数块里new一个对象,可以将该对象的指

针返回回去,该对象依旧存在。而声明的对象的生命周期只存在于声明了该对象的函数块中,如果返

回该声明的对象,将会返回一个已经被销毁的对象。

new对象指针用途广泛,比如作为函数返回值、函数参数等``。

2、引用

本质:给变量起别名

语法:(和原来一致的)数据类型 &别名 = 原名

  • 修改引用也会修改原来的变量,因为操纵的是一块内存
  • 引用必须初始化
  • 一个变量可以有多个引用,一个引用只能指向一个实体
int a;
int & b = a;//b为a的引用
//int & a_num;错误的,引用避暑初始化

1、将引用作为函数参数

void swapv(int a,int b); // 值传递                不能改变调用函数中的值
void swapr(int & a,int & b); // 引用传递        能改变调用函数中的值
void swapp(int * a,int * b); // 指针传递          能改变调用函数中的值

2、引用做函数的返回值

a.不要换返回局部变量的引用
int &test()//例如这个函数就是不合法的
{
    int a = 10;//局部变量存放在栈区,函数执行完毕就被释放
    return a;
}
b.函数的调用可以作为左值
int &test_02()
{
    static int a = 14;//静态变量,存放在全局区,全局区上的数据在整个程序结束由系统释放释放
    return a;//返回a的引用
}

int main()
{
    int &ref_02 = test_02();//ref_02相当于引用的a
    cout << ref_02 << endl;//结果为14
    //无论是ref_02还是test_02都是a的引用,都可以对a的内存进行操作
    test_02() = 133
    out << ref_02 << endl;//结果为133
    cout << test_02() << endl;//结果为133
    system("pause");
    return 0;
}

3、常量引用(引用时尽可能使用const)

  • 使用const能避免无意中修改数据的编程错误
  • 使用const能够处理const和非const实参,否则只能接受非const数据
  • 使用const引用使函数能正确的生成并使用临时变量

作用:主要用来修饰形参,防止误操作,在函数形参列表中,可以加const修饰形参,防止形参改变实参

//引用使用的场景,通常用来修饰形参
void showvalue(const int&v){
    //y+=10;
    cout << v << endl;
}
int main() {
    //int &ref=10;引用本身需要一个合法的内存空间,因此这行错误
    //加入const就可以了,编译器优化代码,int temp=18;const int&ref=temp;
    const int &ref = 10;//都相当于引用了一个临时变量
    //ref=100;加入const后不可以修改变量
    cout << ref << endl;
    //函数中利用常量引用防止误操作修改实参
    int a =10;
    showValue(a);
}
// 函数声明
double cube(double &a)
{
    a = a * a;
}
// 调用函数
cube(x+2);

当函数cube()的引用参数应该是可以修改的,但是传递的值是(x+2)并不是变量,所以编译时会出现错误。

但是,如果在引用前加上const程序就能运行了,const指定引用不能被修改,所以是否为变量也就不重要了。但是编译器会为其创建一个临时变量,保存(x+2),然后将 a 成为临时变量的引用

那么什么情况下会创建临时变量呢?如果引用参数是const,则编译器会在下面两种情况生成临时变量:

  • 实参的类型正确,但不为左值
  • 实参的类型不正确,但可以转化为右值

4、引用的本质

引用的本质在c++内部实现是一个指针常量

//自动转换为int * const ref = &a;指针常量是指针指向不可改,也说明为什么引用不可更改
int &ref = a;
ref = 20;//内部发现ref是引用,自动帮我们转换为:*ref=20

5、引用总结

使用引用参数的原因:

  1. 修改调用函数中的函数对象

  2. 通过传递引用而不是整个数据对象,提高程序的运行速度

当数据对象较大的时候,第二个原因尤为重要。这些也是使用指针参数的原因。

那么指针参数和引用又有什么区别呢?

引用参数实际上是基于指针的代码的另一个接口,至此,引用展现了它的庐山真面目,其实它和指针在底层实现没什么两样,只是在使用上,有时引用会更加顺手。

那么什么情况下,应该使用何种传递参数的方式呢?

  • 数据对象小且不做修改,例如内置类型和小型结构,按值传递。
  • 数据对象数组,只能使用指针传递。
  • 结构,引用和指针都可以
  • 类,使用引用

九、函数高级

1、函数默认参数

int func(int a = 1, int b = 2, int c = 3){
    return a + b + c;
}
int main{
    func(4,5)//结果是4+5+3=12
}

如果函数有默认值,还没有传对应的值,那么用默认值;如果有传(对应的)值,那么优先用传值

C++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值。

  • 如果函数声明有默认参数,函数实现就不能有默认参数;函数声明和函数实现智能有一个有默认参数

  • 默认参数除了是可以是常量还可以是变量

2、函数占位参数

语法:函数类型 函数名(参数类型)

目前了解,还用不到,以后会用到

void func(int,float = 10.12);//占位参数还可以有默认参数

3、函数重载

作用:函数名可以相同,提高复用性

函数重载满足条件:

  • 同一个作用于下(目前写的函数都是全局函数)
  • 函数名相同
  • 函数参数类型不同或者个数不同或者顺序不同
void swap(float &a, float &b)
{
    double tmp = a;
    a = b;
    b = tmp;
}
void swap(int &a, int &b) // 函数重载
{
    double tmp = a;
    a = b;
    b = tmp;
}
int main()
{
    int x = 12;
    int y = 15;
    float fx = 1.22;
    float fy = 3.14;
    swap(fx, fy);
    swap(x, y);
    cout << x << " " << y << endl;
    cout << fx << " " << fy << endl;
}

函数的返回值不能作为函数重载的条件

void func(float a;double b){}
int func(float a;double b){}//函数的返回值不能作为函数重载的条件

a.引用作为重载的注意事项

void fun(float &a, )
{
    cout  << "对fun(float)的调用" << endl;
}
void fun(const float &a) 
{
    cout  << "对fun(const float)的调用" << endl;
}
int main(){
    int a = 10;
    fun(a);//对fun(float)的调用
    fun(10);//对fun(const float)的调用,有const会在创建临时变量接收
}

b.函数重载遇到默认参数

void fun(float a)
{
    cout  << "对fun(float a)的调用" << endl;
}
void fun(float a,int b  = 10) //有默认参数就可以忽略不看,对比剩下的参数
{
    cout  << "对fun(float a, int b  = 10)的调用" << endl;
}
int main(){
    int a = 10;
    fun(a);//此时对两个fun()都可以调用,会报错
}

函数重载时,尽量不要使用默认参数,很容易出现二义性导致报错

十、类和对象

1、封装

1.封装的意义

const double PI = 3.14;
class Cricle
{
public://访问权限,公共类
    int r ;//属性
    double cricle_grith()//行为
    {
        return 2 * PI * r;
    }
};

int main()
{
    Cricle c_1;//创建对象
    c_1.r = 3.3;//给属性赋值
    cout << c_1.cricle_grith() << endl;
}

语法:class 类名 { 访问权限: 属性 / 行为 };

class Stu
{
public://访问权限,公共类
    string stu_name;
    string stu_num;
    void show_stu_info()
    {
        cout << stu_name << endl;
        cout << stu_num << endl;
    }
    void setName(string name)//用成员函数给对象属性复制
    {
        stu_name = name;
    }
    void setNum(string num)//用成员函数给对象属性复制
    {
        stu_num = num;
    }
};
int main()
{
    Stu stu_1;
    //stu_1.stu_name = "小明";
    //stu_1.stu_num = "201902250417";
    stu_1.setName("小红");
    stu_1.setNum("201902250417");
    stu_1.show_stu_info();
}

封装权限:

1.公共权限:public 类内可以访问,类外可以访问

2.保护权限:protected 类内可以访问,类外不可以访问,子类可以访问

3.私有权限:private 类内可以访问,类外不可以访问,子类不可以访问

2.struct和class的区别

默认的访问权限不同

  • struct默认权限为公共
  • class默认权限为私有

3、将成员属性私有化

  • 将成员属性私有化,可以自己控制读写权限
  • 对于写权限,我们可以监测数据的有效性
class Stu
{
public://访问权限,公共类
    string getName()//可读
    {
        return stu_name;}
    void setName(string name)//可写
    {
        stu_name = name;}
    void setNum(string num)//只可写
    {
        stu_num = num;}
    int getAge(int age)//只可读
    {
        age = 0;//设置默认值
        return stu_age;}
private://成员属性设置为私有类,类外部无法访问,可以用过公有化访问权限进行读写
    string stu_name;//可读可写
    string stu_num;//只可写
    int stu_age;//只可读
};
class Stu
{
public://访问权限,公共类
    void setNum_show(string num)//可写
    {
        if(num.size() == 12)//可以检测数据的合法性
        {
            stu_num = num;
            cout << "输入的学号为: " << stu_num << " 请确认" << endl;}
        else
        {
            cout << "学号输入错误,请重新输入" << endl;}
    }
private:
    string stu_num;//只可写
};
int main()
{
    Stu s1;
    s1.setNum_show("201902250417");//输入的学号为: 201902250417 请确认
    //s1.setNum_show("学号");学号输入错误,请重新输入
}

注意:定义全局函数(在class外定义的)和成员函数(在class内定义的)定义时传参的不同;成员函数需要有一个对象进行调用

在类中可以让另一个类,作为本类中的成员

4、将类分文件编写

头文件(.h):

class Stu
{
public:
    //声明成员函数:void setNum(string num);
    //声明成员函数:void setNum_show(string num);
    //...
private:
    //成员属性:string stu_num;
    //成员属性:string stu_num;
    //成员属性:string stu_num;
    //...
};

原文件(.cpp):

#include "xxx.h"
void Student::setNum(string num)//写函数实现,和作用域
{
    //...
}
void Teacher::setNum_show(string num)
{
    //...
}
//...

不加作用域会被认为是全局函数

2、对象的初始化和清理

1.构造函数和析构函数

C++利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现

  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

构造函数语法:类名(){}

  1. 构造函数,没有返回值也不写void

  2. 函数名称与类名相同

  3. 构造函数可以有参数,因此可以发生重载

  4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次

析构函数语法:~类名(){}

  1. 析构函数,没有返回值也不写void

  2. 函数名称与类名相同,在名称前加上符号~

  3. 析构函数不可以有参数,因此不可以发生重载

  4. 程席在对象销毁前会自动调用析构.无须手动调用.而且只会调用一次

2.构造函数的分类及调用

分为无参构造有参构造;也可以分为普通构造拷贝构造

class Person
{
public:
    Person()//构造函数(默认构造函数)
    {cout<< "正在执行默认构造函数" << endl;}
    Person(int a)//有参构造函数
    {
        age = a;
        cout<< "正在执行有参构造函数" << endl;}
    Person(const Person &p)//拷贝构造函数
    {
        age = p.age;
        cout<< "正在执行拷贝构造函数" << endl;}
    ~Person()//析构函数
    {cout<< "正在执行析构函数" << endl;}
private:
    int age;
};
void fun_p()//调用
{
    //1、括号法
    Person p;//默认构造函数调用
    Person p1(2);//有参构造函数调用
    Person p2(p1);//拷贝构造函数调用
    //2、显示法
    Person p3 = Person(2);//有参构造函数调用
    Person p4 = Person(p3);//拷贝构造函数调用
    //3、隐式转换法
    Person p5 = 10;//有参构造函数调用
    //相当于Person p5 = Person(10);
    Person p6 = p5;//拷贝构造函数调用
}
int main()
{
    fun_p();}

Person(10);

匿名对象:当前执行结束会立即被系统回收(执行析构函数)

Person(p3);

不要利用拷贝构造函数,舒适化匿名对象,编译器会认为是一个对象声明(Person(p3)==Person p3;)

调用默认构造函数时候,不要加()

因为下面这行代码,编译器会认为是一个函数的声明

Person p1();

3.拷贝构造函数调用时机

C++中拷贝构造函数调用时机通常有三种情况

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象

4.构造函数调用规则

默认情况下,C++编译器至少给一个类添加3个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,C++不在提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,C++不会再提供其他构造函数

5.深拷贝与浅拷贝

浅拷贝:简单的赋值拷贝操作

深拷贝:在堆区重新申请空间,进行拷贝操作

编译器提供的拷贝构造函数会做浅拷贝操作

如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

6.初始化列表

用来初始化属性

class Person
{
public:
    Person(int x,int y,int z):mA(x),mB(y),mC(z)//初始化列表
    {

    }
    int mA;
    int mB;
    int mC;
};
void fun_p()//调用
{
    Person p(3,2,1);
}

7.类对象作为类成员

C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员

class A {}
class B {
    A a;
}

对象成员会先构造自身类(class A),再构造本类(class B)

对象成员会先析构本类(class B),再析构自身类(class A)

构造和析构的顺序:A构造 -> B构造 -> B析构 -> A析构

8.静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员

静态成员分为:

  • 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量
class Person
{
public:
    static int mA;//类内声明
private:
    static int mB;//静态成员变量也是有访问权限的,在类外就不能访问
};
int Person::mA = 11;//类外初始化
int Person::mB = 13;
int main()
{
    Person p;
    Person p1;
    p1.mA = 12;//共享同一份数据
    cout << p.mA << endl;//结果为12
    cout << Person::mA << endl;//结果为12
}

静态成员变量不属于某个对象,所有对象共享一份数据

静态成员变量有两种访问方式:

  1. 通过对象进行访问:p1.mA

  2. 通过类名进行访问:Person::mA

#include 
#include 
using namespace std;

class Person
{
public:
    static void func(int test_dm)//静态成员函数
    {
        //dm = test_dm;无法访问非静态成员变量
        mm = test_dm;
        cout << "静态成员函数被调用" << endl;
    }
    static int mm;//声明一个静态成员变量
    int dm;//声明一个普通成员变量
private:
    static void private_func()//私有静态成员函数不能被类外部访问,也是有访问权限的
    {
        cout << "私有静态成员函数被调用" << endl;
    }
};
int Person::mm = 10;
void assign()
{
    //静态成员变量的调用方式
    Person p;
    p.func(6);//1、通过对象调用
    Person::func(5);//2、通过类名调用
}

int main()
{
    assign();
    Person test_p;
    cout << test_p.mm << endl;//可以正常使用,结果为5
}

3、C++对象模型和this指针

1、成员变量和成员函数的分开储存

类内的成员变量和成员函数分开存储;只有非静态成员变量才属于类的对象上

空对象占用内存空间为:1

C++编译器会给每个空对象分配一个字节的空间,是为了区分空对象的内存位置

2、this指针的概念

每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这一块代码是如何区分那个对象调用自己的呢?

C++通过提供特殊的对象指针,this指针指向被调用的成员函数所属的对象

this指针是隐含每一个非静态成员函数内的一种指针

this指针不需要定义,直接使用即可

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return*this
class Person
{
public:
    Person(int m_age)//有参构造函数
    {
        //m_age = m_age;命名冲突
        this->m_age = m_age;//用this指针就可以解决
    }
    Person &PersonAdd(Person &p)//如果要返回对象就要用引用类型
    //Person PersonAdd(Person &p)用值的方式返回,会创建新的对象
    {
        this->m_age += p.m_age;
        return *this;//返回的是对用对象本身
    }
    int m_age;
};
void test_1()
{
    Person p(18);
    cout << p.m_age << endl;
}
void test_2()
{
    Person p1(3);
    Person p2(4);
    //链式编程思想
    p2.PersonAdd(p1).PersonAdd(p1);//因为返回的是调用对象本身,所以可以一直追加
    cout << p2.m_age << endl;//结果就为10
}
int main()
{
    test_1();
    test_2();
}

3.空指针调用成员函数

class Person
{
public:
    void showName()
    {
        cout << "this is Person class" << endl;
    }
    void showPersonage()
    {
        //报错原因是因为传入指针为空
        if(this == NULL)//提高代码健壮性做空指针判断
        {
            return;
        }
        cout << m_Age << endl;//相当于cout << this->m_Age << endl;
    }
    int m_Age;
};
void test_1()
{
    Person *p = NULL;
    p->showName();
    p->showPersonage();//空指针无法访问成员,就是有this指针就不能访问
}
int main()
{
    test_1();
}

4.const修饰成员函数

常函数:

  • 成员函数后加const后我们称为这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改

常对象:

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数
class Person
{
public:
    void showPersonage() const//常函数,此const本质修饰的是this指针
    {
        //m_Age  = 18;m_Age就不能被修改
        //相当于this->m_Age
        //指针的本质是指针常量:指向是不能修改的(指向调用对象)
        m_Age_cost = 99;
    }
    void commen_fun()
    {    }
    int m_Age;
    mutable int m_Age_cost;//有mutable修饰的是特殊变量,可以在常函数中修改
};
void test_1()
{
    const Person p;//常对象
    //p.m_Age=100;
    p.m_Age_cost=100;//m_Age_cost是特殊值,在常对象下也可以修改
    //p.commen_fun();再普通函数中可以修改属性,所以常对象不能调用普通函数
}
int main()
{
    test_1();
}

4、友元

友元的目的就是让一个函数或者类访问另一个类中私有成员

友元的关键字为friend

友元的三种实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

1.全局函数作友元

class Building
{
    //frirnd_fun全局函数是Bui1ding好朋友,可以访问Building中私有成员
    friend void frirnd_fun(Building &building);//如此定义友元
    //...
}

2.类作友元

class Building;//避免Building *building;报错,要提前声明一下
class GoodGay
{
public:
    GoodGay();
    void visit();//参观函数访问Building中的属性
    Building *building;
};
class Building
{
    //GoodGay 设置为友元
    friend class GoodGay;
public:
    Building();
public:
    string m_SittingRoom;//客厅
private:
    string m_BedRoom;
};
Building::Building()//类外编写构造函数
{
    m_SittingRoom="客厅";
    m_BedRoom="卧室";
}
GoodGay::GoodGay()//类外编写构造函数
{
    //创建建筑物对象
    building = new Building;
}
void GoodGay::visit()//类外编写成员函数
{
    cout << "friend正在访问" << building->m_SittingRoom << endl;
    cout << "friend正在访问" << building->m_BedRoom << endl;
}
void test_1()
{
    GoodGay gay;
    gay.visit();
}
int main()
{
    test_1();
}

3.成员函数作友元

//告诉编译器GoodGay类下的visit成员函数作为本类的好朋友,可以访问私有成员
class Building
{
    friend void GoodGay:visit();
    //...
}

5、运算符重载

实现两个自定义数据类型的运算

1.加号运算符重载

class Person
{
public:
    //1、成员函数重载+号
    Person operator+(Person &p)
    {
        Person temp;
        temp.m_A = this->m_A + p.m_A:
        temp.m_B = this->m_B + p.m_B;
        return temp;
    }
    int m_A;
    int m_B;
}
//2、全局函数的+号重载
Person operator+(Person &p1,Person &p2)
{
    Person temp;
    temp.m_A = p1.m_A + p2.m_A;
    temp.m_B = p1.m_B + p2.m_B;
    return temp;
}
//运算符重载,可以发生函数重载
Person operator+(Person &p1,int num)
{
    Person temp:
    temp.m_A = p1.m_A + num;
    temp.m_B = p1.m_B + num;
    return temp;
}
void test0l()
{
    Person p1;
    p1.mA=10:
    p1.mB=10:
    Person p2;
    p2.mA=10:
    p2.mB=10:
    Person p3 =pl + p2;//+号重载之后才不会报错
}

成员函数重载本质调用

Person p3 p1.operator+(p2);

全局函数重载本质调用

Person p3 operator+(p1,p2);

2.左移运算符重载

利用成员函数重载左移运算符 p.operator << (cout) 简化版本 p << cout

不会利用成员函数重载<运算符,因为无法实现cout在左侧

智能利用全局函数重载左移运算符

class Person{
    friend ostream &operator<<(ostream &out,Person &p);//设置为友元
public:
    Person(int a,int b)
    {
        this->m_A = a;
        this->m_B = b;
    }
    //成员函数实现不了 p << cout 不是我们想要的效果
    //void operator<<(Person&p){}
private:
    int m_A;
    int m_B;
};
ostream&operator<<(ostream&cout,Person&p)//本质operator<<(cout,p)
{
    cout << "mA=" << p.mA << "mB=" << p.mB;
    return cout;
}

3.递增运算符重载

class MyInteger
{
public:
    MyInteger()
    {
        m_Num =0;//初始化属性
    }
    //重载前置++运算符
    MyInteger & operator++()//返回引用为了一直对一个数据进行递增
    {
        m_Num++;//先做++
        return *this;//再做返回
    }
    //重载后置++运算符
    MyInteger operator++(int)//用于区分后置,int代表占位运算符
    {
        MyInteger temp = *this;//局部对象,运行完直接被删除无法返回对象
        m_Num++;//先做++
        return temp;//再做返回
}
private:
    int m_Num;
};
void test_l()
{
    MyInteger myint;
    cout << ++myint << endl;
}
int main()
{
    test_1();
}

4.赋值运算符重载

C++编译器至少给一个类添加4个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符operator=,对属性进行值拷贝

如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题

class Person{
public:
    Person(int age)
    {
    m_Age = new int(age);//将年龄数据开辟到堆区
    }
    Person &operator = (Person &p)//重载赋值运算符
    {
        if (m_Age != NULL)
        {
            delete m_Age;
            m_Age = NULL;
        }
        //编泽器提供的代码是浅拷贝
        //m_Age =p.m_Age;
       
        m_Age = new int(*p.m_Age); //提供深拷贝解决浅拷贝的问题
        return *this;//返回自身
    }
    ~Person()//析构函数清理内存
    {
        if (m_Age !NULL)
        {
            delete m_Age;
            m_Age NULL;
        }
}
int *m_Age;//年龄的指针
};
void teste1()
{
    Person p1(18);
    Person p2(20);
    Person p3(30);
    p3=p2=p1;//赋值操作
    cout<<"p1的年龄为:"<< *p1.m_Age << end1;
    cout<<"p2的年龄为:"<< *p2.m_Age << end1;
    cout<<"p3的年龄为:"<< *p3.m_Age << end1;//此时p1、p2、p3应该都为18
}

5.关系运算符重载

6.函数调用运算符重载

class Myfun
{
public:
    int operator()(int num1,int num2)
    {
        return num1+num2;
    }
};
void test_1()
{
    Myfun fun1;
    int result = fun1(12,15);//也称为仿函数,在stl经常使用
    cout << result << endl;
    //匿名函数对象,Myfun()用完就会销毁代替了fun1
    //匿名对象:类名+小括号
    cout << Myfun()(10,13) << endl;
}
int main()
{
    test_1();
}

6、继承

语法:class 子类 : 继承方式(public) 父类

1.继承方式

  • 公共继承(public)
  • 保护继承(protected)
  • 私有继承(private)
继承逻辑

1、私有属性,无论哪种继承都无法访问

2、公有继承:父类到子类属性权限无变化

3、保护继承:父类公有变子类保护

4、私有继承:父类公有、保护变子类私有

父类中的私有属性也会被继承,只是被隐藏无法访问

2.继承中的构造与析构顺序

当创造子类创造对象,也会调用父类的构造函数

class Base
{
public:
    Base()//基类构造函数
    {cout << "Base的构造函数" << endl;}
    ~Base()//基类析构函数
    {cout << "Base的析构函数" << endl;}
};
class Son : public Base
{
public:
    Son()//子类构造函数
    {cout << "Son的构造函数" << endl;}
    ~Son()//子类析构函数
    {cout << "Son的析构函数" << endl;}
};
void test_1()
{
    Son son1;
}
//运行结果
//Base的构造函数
//Son的构造函数
//Son的析构函数
//Base的析构函数

3.继承同名成员处理方式

  • 访问子类同名成员,直接访问即可

  • 访问父类同名成员,需要加作用域

class Base
{
public:
    Base()
    {m_age = 155;}
    int m_age;
};
class Son : public Base
{
public:
    Son()
    {m_age = 156;}
    int m_age;
};
void test_1()
{
    Son son1;
    son1.Base::m_age;
    cout << son1.Base::m_age << endl;//可以访问到155
}

4.继承同名静态成员处理方式

  • 访问子类同名静态成员,直接访问即可

  • 访问父类同名静态成员,需要加作用域

  • 静态成员还可以直接通过类名访问

  • 访问静态函数同理

class Base
{
public:
    Base()
    {m_age = 15;}
    static int m_age;
};
int Base::m_age;
class Son : public Base
{
public:
    Son()
    {m_age = 156;}
    int m_age;
};
void test_1()
{
    Son son1;
    son1.Base::m_age;
    cout << son1.Base::m_age << endl;//通过创建对象访问   结果为15
    cout << Son::Base::m_age << endl;//通过类名访问       结果为15
    //只有静态成员可以通过类名访问
    //cout << Son::m_age << endl;//会报错,非静态成员不可以
}

5.多继承语法

C++允许一个类继承多个类工

语法:c1ass 子类:继承方式父类1,继承方式父类2…

多继承可能会引发父类中有同名成员出现,需要加作用域区分,C++实际开发中不建议用多继承

6.菱形继承

两个派生类继承同一个基类,又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石继承

问题:

1、两个孙子类继承了两份基类的属性,会产生二义性,需要加作用域解决

2、孙子类中的基类属性,要通过**虚继承(virtual关键字)**,孙子类用该属性最新赋予的数据

class sheep :virtual public Animal {};

此时,class Animal{};称为虚基类

十一、多态

多态是C++面向对象三大特性之一

多态分为两类

  • 静态多态:函数重载和运算符重载属于静态多态,复用函数名

  • 动态多态:派生类和虚函数实现运行时多态

静态多态和动态多态区别:

  • 静态多态的函数地址早绑定:编译阶段确定函数地址

  • 动态多态的函数地址晚绑定:运行阶段确定函数地址

class Base_animal
{
public:
    virtual void speak()//虚函数
    {cout << "发出了动物叫" << endl;}
};
class Cat : public Base_animal
{
public:
    //此(派生类)virtual可写可不写,基类必须写
    virtual void speak()//重写函数:返回值 函数名 参数列表要完全一致
    {cout << "发出了猫叫" << endl;}
};
void Speak(Base_animal &animal)//基类的引用指向->派生类的对象
{animal.speak();}
void test_1()
{
    Cat cat;
    Speak(cat);
}

动态多态满足条件:

  1. 有继承关系
  2. 子类重写父类的虚函数

使用:基类指针或者引用指向子类对象

原理:

基类中virtual关键字定义了虚函数,则会生成一个虚函数指针(记录虚函数表中的函数位置),指向虚函数表;在派生类中进行继承基类,包括虚函数指针也会被继承,并且在基类中重写该函数,此时,派生类中的虚函数指针会指向新的虚函数表中的函数位置,当基类指针或引用指向派生类的对象时,会执行派生的虚函数,由此发生多态

特点:组织结构清晰、可读性强、便于扩展和维护

C++开发中建议多用多态结构编写

1、案例一:计算器类

开闭原则:对扩展进行开放,对修改进行关闭

class Calculator//普通方法,编写一个计算器
{
public:
    int getResult(string oper)
    {
        if (oper == "+")
        {return num1 + num2;}
        else if (oper == "-")
        {return num1 - num2;}
        else if (oper == "*")
        {return num1 * num2;}
        else if (oper == "/")
        {return num1 / num2;}
    }
    int num1;
    int num2;
};
void test()
{
    Calculator c1;
    c1.num1 = 23;
    c1.num2 = 12;
    cout << c1.getResult("+") << endl;
}
class Abstract_Calculator//计算器(基)类
{
public:
    virtual int get_Result()//虚函数
    {return 0;}
    int m_Num1;
    int m_Num2;
};
class Plus_Calculator : public Abstract_Calculator//继承
{
public:
    virtual int get_Result()//重写
    {return m_Num1 + m_Num2;}
};
class Sub_Calculator : public Abstract_Calculator//继承
{
public:
    virtual int get_Result()//重写
    {return m_Num1 - m_Num2;}
};
class Mul_Calculator : public Abstract_Calculator//继承
{
public:
    virtual int get_Result()//重写
    {return m_Num1 * m_Num2;}
};
class Division_Calculator : public Abstract_Calculator//继承
{
public:
    virtual int get_Result()//重写
    {return m_Num1 / m_Num2;}
};

void test()
{
    //Abstract_Calculator * abc = new Mul_Calculator;//乘法,创建一个对象在堆区
    //Abstract_Calculator * abc = new Plus_Calculator;//加法,创建一个对象在堆区
    //Abstract_Calculator * abc = new Sub_Calculator;//减法,创建一个对象在堆区
    Abstract_Calculator * abc = new Division_Calculator;//除法,创建一个对象在堆区
    abc->m_Num1 = 75;
    abc->m_Num2 = 2;
    cout << abc->get_Result() << endl;
    delete abc;//记得销毁堆区内存
}
int main()
{
    test();
    system("pause");
    return 0;
}

2、纯虚数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名(参数列表)= 0;

当类中有了纯虚函数,这个类也称为抽象类

抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Abstract_Calculator//抽象列(包含了get_Result()这个纯虚函数),无法实例化对象
{
public:
    virtual int get_Result() = 0;//纯虚函数
    int m_Num1;
    int m_Num2;
};

3、虚析构和纯虚构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象

  • 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:virtual类名( ) { }

纯虚析构语法:

virtual 类名 () = 0 ;

类名::类名( ) { }

class Abstract_Animal//计算器抽象类
{
public:
    Abstract_Animal()
    {cout << "Animal 的构造函数调用" << endl;}
    virtual int speak() = 0;
    virtual ~Abstract_Animal()
    {cout << "Animal 的析构函数调用" << endl;}
    //virtual ~Abstract_Animal() = 0;纯虚析构写法一
    string *m_Name;
};
//Animal :: ~Abstract_Animal()//纯虚析构写法二
//{
//    cout << "Animal 的析构函数调用" << endl;
//}
class Cat : public Abstract_Animal
{
public:
    Cat(string name)
    {cout << "Cat 的构造函数调用" << endl;
        m_Name = new string(name);}
    virtual int speak()
    {cout << *m_Name << " little cat speaking" << endl;}
    ~Cat()
    {
        if (m_Name != NULL)
        {
            delete m_Name;
            m_Name = NULL;
            cout << "Cat 的析构函数调用" << endl;
        }
    }
};
void test()
{
    Abstract_Animal * animal = new Cat("Tommy");
    animal->speak();
    delete animal;
}
//    如果没有加虚析构结果为:
//Animal 的构造函数调用
//Cat 的构造函数调用
//Tommy little cat speaking
//Animal 的析构函数调用
//    加了结果为:
//Animal 的构造函数调用
//Cat 的构造函数调用
//Tommy little cat speaking
//Cat 的析构函数调用
//Animal 的析构函数调用    

4、案例二:电脑组装

#include 
#include 
using namespace std;

class Cpu
{
public:
    virtual void calsulate() = 0;
};
class VidoCard
{
public:
    virtual void display() = 0;
};
class Memory
{
public:
    virtual void storage() = 0;
};
class Computer
{
public:
    Computer(Cpu *cp,VidoCard *vc,Memory *me)
    {
        m_cpu = cp;
        m_vidocard = vc;
        m_memory = me;
    };
    void work()
    {
        m_cpu->calsulate();
        m_vidocard->display();
        m_memory->storage();
    }
    ~Computer()
    {
        if (m_cpu != NULL)
        {
            delete m_cpu;
            m_cpu = NULL;
        }
        if (m_vidocard != NULL)
        {
            delete m_cpu;
            m_vidocard = NULL;
        }
        if (m_memory != NULL)
        {
            delete m_cpu;
            m_memory = NULL;
        }
    }
private:
    Cpu *m_cpu;
    VidoCard *m_vidocard;
    Memory *m_memory;
};
class Intel_Cpu : public Cpu
{
public:
    virtual void calsulate()
    {cout << "CPU of Intel is working" << endl;}
};
class Intel_VideoCard : public VidoCard
{
public:
    virtual void display()
    {cout << "VidoCard of Intel is working" << endl;}
};
class Intel_Memory : public Memory
{
public:
    virtual void storage()
    {cout << "Memory of Intel is working" << endl;}
};
class Lenovo_Cpu : public Cpu
{
public:
    virtual void calsulate()
    {cout << "CPU of Lenovo is working" << endl;}
};
class Lenovo_VideoCard : public VidoCard
{
public:
    virtual void display()
    {cout << "VidoCard of Lenovo is working" << endl;}
};
class Lenovo_Memory : public Memory
{
public:
    virtual void storage()
    {cout << "Memory of Lenovo is working" << endl;}
};
void test()
{
    Cpu *intel_cpu = new Intel_Cpu;//第一台电脑零件
    VidoCard *intel_vidocard = new Intel_VideoCard;
    Memory *intel_memory = new Intel_Memory;

    Computer *computer1 = new Computer(intel_cpu,intel_vidocard,intel_memory);//组装第一台电脑
    computer1->work();
    delete computer1;

    Computer *computer2 = new Computer(new Lenovo_Cpu,new Lenovo_VideoCard,new Lenovo_Memory);//组装第二台电脑
    computer2->work();
    delete computer2;
}
int main()
{
    test();
    system("pause");
    return 0;
}

十二、文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放,通过文件可以将数据持久化

C++中对文件操作需要包含头文件

文件类型分为两种:

  1. 文本文件:文件以文本的ASCII码形式存储在计算机中

  2. 二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们

操作文件的三大类:

  1. ofstream:写操作

  2. ifstream:读操作

  3. fstream:读写操作

1、文本文件

1.写文件

写文件步骤如下:

  1. 包含头文件

    #include

  2. 创建流对象

    ofstream ofs;

  3. 打开文件

    ofs.open(“文件路径”,打开方式);

  4. 写数据

    of5<<”写入的数据”;

  5. 关闭文件

    ofs.close();

打开方式 作用
ios::in 为读文件而打开文件
ios::out 为写文件而打开文件
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在先删除,在创建
ios::binary 二进制方式
#include 
int main()
{   
    ofstream ofs;//创建流文件
    //以指定模式打开选择文件
    ofs.open("E:\\desktop\\txtfile.txt",ios::app | ios::binary);//可以用两种模式一起打开
    ofs << endl << "想要写入的内容";//可以先换行再写入
    ofs.close();//关闭文件
    return 0;
}

2.读文件

读文件与写文件步骤相似,但是读取方式相对于比较多

读文件步骤如下:

  1. 包含头文件

    #include

  2. 创建流对象

    ifstream ifs;

  3. 打开文件并判断文件是否打开成功

    ifs.open(“文件路径”打开方式):

  4. 读数据

    四种方式读取

  5. 关闭文件

    ifs.close();

#include 
#include 
#include 
using namespace std;
int main()
{   
    ifstream ifs;
    ifs.open("E:\\Desktop\\txtfile.txt",ios::in);
    if (!ifs.is_open())
    {cout << "file is fail to open" << endl;}
    char buf[1024]={0};//第一种
    while (ifs >> buf){cout << buf << endl;}
    
    //char buf[1024]={0};//第二种
    //while (ifs.getline(buf,sizeof(buf))){cout << buf << endl;}
    
    //string buf;//第三种
    //while (getline(ifs,buf)){cout << buf << endl;}

    //char c;//第四种,不推荐效率太低
    //while ((c = ifs.get()) != EOF ){cout << c << endl;}
    ifs.close();
    
    system("pause");
    return 0;
}

2、二进制文件

打开方式指定为:iso::binary

1.写文件

二进制方式写文件主要利用流对象调用成员函数write

函数原型:ostream& write(const char *buffer,int len);

参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数

class Person
{
public:
    char m_Name[64];
    int m_Age;
}
void teste1()//二进制文件写文件
{
    ofstream=ofs("person.txt",ios:out ios:binary);
    //ofs.open("person.txt",ios:out ios:binary);//打开文件
    Person p={"张三",18};
    ofs.write((const char *)&p,sizeof(p));//写文件
    ofs.close();
}

2.读文件

二进制方式读文件主要利用流对象调用成员函数read

函数原型:istream&read(char*buffer,int len);

参数解释:字符指针ouffer指向内存中一段存储空间。len是读写的字节数

class Person{
public:
    char m_Name[64];
    int m_Age;
};
void teste1()
{
    ifstream ifs("person.txt",ios:in ios:binary);
    if(Iifs.is_open())
    {cout << "文件打开失败" << endl;}
    Person p;
    ifs.read((char *)&p,sizeof(p));
    cout << "姓名:" << p.m_Name << "年龄:" << p.mAge << endl;
}

十三、数据类型转换

当运算符的操作数具有不同的数据类型时,C++ 会自动将它们转换为相同的数据类型。当它这样做时,遵循一组规则。

就像军队的军官有军阶一样,数据类型也可以按等级排名。如果一个数字数据类型可以容纳的数字大于另一个数据类型,那么它的排名就高于后者。例如,float 类型就超越了 int 类型,而 double 类型又超越了 float 类型。下表列出了从高到低排列的数据类型。

long double
double
float
unsigned long long int
long long int
unsigned long int
long int
unsigned int
int

上表排名的一个例外是当 int 和 long int 的大小相同时。在这种情况下,unsigned int 将超越 long int,因为它可以保存更高的值。

1.隐式/自动数据类型转换

当 C++ 使用运算符时,它会努力将操作数转换为相同的类型。这种隐式或自动的转换称为类型强制

现在来看一看管理数学表达式评估的具体规则:

  • 规则 1:char、short 和 unsigned short 值自动升级为 int 值。细心的读者可能已经注意到,char、short 和 unsigned short 都未出现在表 1 中,这是因为无论何时在数学表达式中使用这些数据类型的值,它们都将自动升级为 int 类型。

  • 规则 2:当运算符使用不同数据类型的两个值时,较低排名的值将被升级为较高排名值的类型。在下面的表达式中,假设 years 是一个 int 变量,而 interestRate 是一个 double 变量

    years * interestRate 
    

    在乘法发生之前,years 中的值将升级为 double 类型。

  • 规则 3:当表达式的最终值分配给变量时,它将被转换为该变量的数据类型。在下面的语句中,假设 area 是一个 long int 长整型变量,而 length 和 width 都是 int 整型变量:

    area = length * width;
    

因为存储在 length 和 width 中的值是相同的数据类型,所以它们都不会被转换为任何其他数据类型。但是,乘法的结果将被升级为 long int 类型,这样才可以存储到 area 中。

但是,如果接收值的变量的数据类型低于接收的值,那该怎么办呢?在这种情况下,值将被降级为变量的类型。如果变量的数据类型没有足够的存储空间来保存该值,则该值的一部分将丢失,并且该变量可能会收到不准确的结果。

我们知道,如果接收值的变量想要的是一个整数,而赋给它的值是一个浮点数,那么当转换为 int 并存储在变量中时,浮点值将被截断。这意味着小数点后的所有内容都将被丢弃。示例如下:

int x;
double y = 3.75;
x = y; // x被赋值为3,y仍然保留3.75

但是,重要的是要了解,当变量值的数据类型更改时,它不会影响变量本身。例如,来看下面的代码段。

int quantity1 = 6;
double quantity2 = 3.7;
double total;
total = quantity1 + quantity2;

在 C++ 执行上述加法之前,它会将一个 quantity1 值的副本移动到其工作空间中,并将其转换为 double 类型。然后把 6.0 和 3.7 相加,并且将结果值 9.7 存储到 total 中。但是,变量 quantity1 保持为 int,存储在存储器中的值保持不变,它仍然是整数 6。

2.手动强制转换

有时程序员想要自己更改值的数据类型,这可以通过使用类型强制转换表达式来完成。类型强制转换表达式允许手动升级或降级值。它的一般格式如下:

static_cast(Value)

其中 Value 是要转换的变量或文字值,DataType 是要转换的目标数据类型。以下是使用类型转换表达式的代码示例:

double number = 3.7;
int val;
val = static_cast(number);

上述代码定义了两个变量:double 类型的 number 和 int 类型的 val。第 3 个语句中的类型转换表达式返回 number 中的值的副本,转换为 int。当 double 或 float 类型的值转换为 int 时,小数部分被截断,因此该语句将 3 存储在 val 中。而 number 的值仍为 3.7,保持不变。

类型转换表达式在 C++ 不能自动执行所需转换的情况下很有用。

下面的程序显示了使用类型强制转换表达式来防止发生整除法的示例。

//This program uses a type cast to avoid an integer division.
#include 
using namespace std;
int main()
{    
    int books, months;
    double booksPerMonth;
    // Get user inputs    
    cout << "How many books do you plan to read? ";    
    cin >> books;    
    cout << "How many months will it take you to read them? ";    
    cin >> months;    
    // Compute and display books read per month    
    booksPerMonth = static_cast(books) / months;    
    cout << "That is " << booksPerMonth << " books per month.\n";    
    return 0;
}

程序输出结果:

How many books do you plan to read? 30
How many months will it take you to read them? 7
That is 4.28571 books per month.

其中,使用类型强制转换表达式的语句是:

booksPerMonth = static cast(books) / months;

变量 books 是一个整数,但是它的值的副本在除法运算之前被转换为 double 类型。如果没有此类型转换表达式,则将执行整除法,导致错误的答案。值得一提的是,如果按以下语句改写此行,则整除法仍然会发生:

booksPerMonth = static_cast  (books / months);

因为括号内的操作在其他操作之前完成,所以除法运算符将对其两个整数操作数执行整除法,books / month 表达式的结果将是 4,然后将 4 转换为 double 类型的值 4.0,这就是将赋给 booksPerMonth 的值。

警告,为了防止发生整除法,在除法运算之前,其中一个操作数应该转换为一个 double 双精度值。这将强制 C++ 自动将其他操作数的值也转换为双精度值。

下面的程序显示了类型强制转换的另一种用法:

//This program prints a character from its ASCII code.
#include 
using namespace std;
int main()
{    
int number = 65;    
//Display the value of the number variable    
cout << number << endl;    
// Use a type cast to display the value of number    
// converted to the char data type    
cout << static_cast(number) << endl;    
return 0;
}

程序输出结果:

65
A

现在来仔细看一看这个程序。首先,int 变量 number 的值被初始化为值 65,同时将 number 发送到 cout,导致显示 65。随后,类型强制转换表达式用于将 number 的值转换为 char 数据类型,再将其发送到 cout。我们知道,字符作为整数 ASCII 代码存储在内存中。因为数字 65 是字母 A 的 ASCII 码,所以最后的输出语句会显示字母 A。

注意,C++ 提供了若干种不同类型的强制转换表达式。static_cast 是最常用的类型强制转换表达式。

3.C 风格和预标准 C++ 类型强制转换表达式

虽然 static_cast 是目前使用最多的类型强制转换表达式,但是 C++ 还支持两种较旧的形式,这也是程序员应该有所了解的,即 C 风格形式和预标准 C++ 形式。

C 风格的转换将要转换的数据类型放在括号中,位于值要转换的操作数的前面。因为类型转换运算符在操作数前面,所以这种类型转换表示法被称为前缀表示法,示例如下:

booksPerMonth = (double)books / months;

预标准 C++ 形式类型强制转换表达式也是将要转换的数据类型放在其值要转换的操作数之前,但它将括号放在操作数周围,而不是围绕数据类型。这种类型转换表示法被称为功能性表示法,示例如下:

booksPerMonth = double(books) / months;

十四、模板

本阶段主要针对C++泛型编程STL技术讲解

  • 模板不可以直接使用,只是一个框架

  • 模板的通用并不是万能的

1、函数模板

  • C++另一种编程思想就是==泛型编程==,主要利用的技术就是==模板==

  • C++提供两种模板机制:函数模板和类模板

函数模板作用:简历一个通用函数,其函数返回值类型形参类型可以不具体指定,用一个虚拟的类型来代表

本质:类型 参数化

语法:

template//typename可以用class代替;T代表通用数据类型,名称可以替换,通常为大写字母
...
函数声明或定义
template 
//template 同理,效果一致
void swapObject(T &a, T &b)
{
    T tmp = a;
    a = b;
    b = tmp;
}
void test01()
{
    int a = 10;
    int b = 20;
    //swapObject(a,b);//1、自动类型推导
    swapObject(a,b);//2、显示指定类型,尖括号指定的是T的类型
}
  • 自动类型推导,必须推导出一致的数据类型T,才可以使用
  • 模板必须要确定出T的数据类型,才可以使用
template
void func()
{
    cout << "func调用" << endl;
}
void test()
{
    func();//此时不能自动识别类型,必须要指定
}

1.案例

template
void select_sort(T *arr, int len)//选择排序
{
    for (int i =0; i arr[max])//判断元素与当前最大下标的大小
            {max = j;}//如果比当前的max大,则赋给max
        }
        if (max != i)//如果最大下标与当前下标不一致,就交换数据
        {
            int tmp = arr[i];
            arr[i] = arr[max];
            arr[max] = tmp;
        }
    }
}
template
void print_result(T *parr, int len)//数组传值需要用指针
{
    for (int i = 0; i < len; i++)//用指针打印数组
    {
        cout << *parr << " ";
        parr++;
    }
    cout << endl;
}
void test_char()
{
    char arr_char[] = "xkudbawlseozmqvpc";
    int len_char = sizeof(arr_char) / sizeof(char);
    //print_result(arr, len);//测试代码
    select_sort(arr_char, len_char);//排序函数调用
    print_result(arr_char, len_char);//打印函数调用
}
void test_int()
{
    int arr_int[] = {14, 46, 26, 33, 6, 12, 37, 24, 11, 9, 2, 0};
    int len_int = sizeof(arr_int) / sizeof(int);
    //print_result(arr, len);//测试代码
    select_sort(arr_int, len_int);//排序函数调用
    print_result(arr_int, len_int);//打印函数调用
}
int main()
{
    test_char();
    test_int();
    system("pause");
    return 0;
}

2.普通函数和函数模板的区别

普通函数与函数模板区别:

  • 普通函数调用时可以发生自动类型转换(隐式类型转换)

  • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换

  • 如果利用显示指定类型的方式,可以发生隐式类型转换

int myAdd0l(int a,int b)//普通函数
{
    return a + b;
}
void test010()
{
    char a = 'a';//会发生自动(隐式)类型转换
    int b = 20;
    cout << myAdd0l(a,b) << endl;
}
template//模板函数
int myAdd0l(T a,T b)
{
    return a + b;
}
void test010()
{
    char a = 'a';//不会发生自动(隐式)类型转换
    int b = 20;
    //cout << myAdd0l(a,b) << endl;//报错如下图
    cout << myAdd0l(a,b) << endl;//这样就不会报错
}
报错

3.普通函数和模板函数的调用规则

调用规则如下:

  1. 如果函数模板和普通函数都可以实现,优先调用普通函数
  2. 可以通过空模板参数列表来强制调用函数模板
  3. 函数模板也可以发生重载
  4. 如果函数模板可以产生更好的匹配,优先调用函数模板
int myAdd0l(int a,int b)
{
    cout << "调用普通函数" << endl;
    return a + b;
}
template
int myAdd0l(T a,T b)
{
    cout << "调用模板函数" << endl;
    return a + b;
}
template
int myAdd0l(T a,T b,T c)//重载的模板
{
    cout << "调用模板函数" << endl;
    return a + b;
}
void test01()
{
    int a = 133;
    int b = 20;
    cout << myAdd0l(a,b) << endl;//会调用普通函数
    cout << myAdd0l<>(a,b) << endl;//强制调用模板函数
    cout << myAdd0l(a,b,155) << endl;//调用重载模板函数
    char x = 'y';
    char y = 'x';
    cout << myAdd0l(x,y) << endl;//调用模板函数,如果调用普通函数还需要转换类型,模板函数能更好匹配则会调用模板函数
}

4.模板函数的局限性

  1. 如果模板函数中有赋值操作,而传入的是数组就无法实现

  2. 利用具体化Person的版本实现代码,具体化优先调用

template<> bool myCompare(Person &p1,Person &p2)
{
    if (p1.m Name ==p2.m Name &p1.m Age ==p2.m Age)
    {
        return true;
    }
    else
    {
        return false;
    }
}
  • 利用具体化的模板,可以解决自定义类型的通用化
  • 学习模板并不是为了写模板,而是在STL能够运用系统提供的模板

2、类模板

建立一个通用类,类中的成员数据类型可以不具体制定,用一个虚拟的类型来代表。

语法:

template
class classname{}
template 
class Person
{
public:
    Person(NameType name, AgeType age)//可以定义两种虚拟类型
    {
        this->m_name = name;
        this->m_age = age;
    }
    NameType m_name;
    AgeType m_age;
};
void test(string name, int age)
{
    Person p1(name, age);//需要指明虚拟类型
    cout << p1.m_name << endl;
    cout << p1.m_age << endl;
}
int main()
{
    test("tommy", 30);
    system("pause");
    return 0;
}

1.类模板与函数模板的区别

  1. 类模板没有自动类型推导的使用方式
  2. 类模板在模板参数列表中可以有默认参数
template
//Person p1("Tommy",20)类模板不会自动识别,无法实现
//Person p2("Tommy",20)必须指定
template
//Person p2("Tommy",20)有默认类型,可以不用写

2.类模板的中成员函数创建时机

类模板中成员函数和普通类中成员函数创建时机是有区别的:

  • 普通类中的成员函数一开始就可以创建
  • 类模板中的成员函数在**调用时(也就是确定了数据类型)**才创建

class Person1
{
public:
    void showPerson1()
    {cout << "Person1 show" << endl;}
};
class Person2
{
public:
    void showPerson1()
    {cout << "Person2 show" << endl;}
};
template
class MyClass
{
public:
    T Obj;
    void func1()//类模板中的成员函数
    {Obj.showPerson1();//在没有调用的时候不会创建,所以不会报错
    }
    void func2()
    {Obj.showPerson2();
    }
};
int main()
{
    MyClass m;
    m.func1();//确定了m是Person1类型,可以执行showPerson1()
    //m.func2();//"showPerson2"不是Person1的成员函数,无法实现
    system("pause");
    return 0;
}

3.类模板对象多函数参数

一共有三种传入方式:

  1. 指定传入的类型 ——直接显示对象的数据类型(实际开发最常用)
  2. 参数模板化 ——将对象中的参数变为模板进行传递
  3. 整个类模板化 ——将这个对象类型模板化进行传递
#include 
#include 
using namespace std;

template
class Person
{
public:
    Person(T1 name,T2 age)
    {
        this->m_name = name;
        this->m_age = age;
    }
    void showPerson1()
    {
    cout << "\"PrintPerson show\" name is " << m_name << " and age is " << m_age<< endl;
    }
    T1 m_name;
    T2 m_age;
};
//1、指定参数传入类型
void PrintPerson(Person&p)
{
    p.showPerson1();
}
void test1()
{
    Personp("Tommy",23);
    PrintPerson(p);
}
//2、参数模板化
template
void PrintPerson(Person&p)
{
    p.showPerson1();
}
void test2()
{
    Personp("Emily",19);
    PrintPerson(p);
}
//3、整个类模板化
template
void PrintPerson(T &p)
{
    p.showPerson1();
}
void test3()
{
    Personp("Adam",33);
    PrintPerson(p);
}
int main()
{
    test1();
    test2();
    test3();
    system("pause");
    return 0;
}
打印变量的数据类型:
cout << typeid(T1).name() << endl;

5.类模板与继承

当类模板碰到继承时,需要注意一下几点:

  • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
  • 如果不指定,编译器无法给子类分配内存
  • 如果想灵活指定出父类中T的类型,子类也需变为类模板
template
class Base
{
    T m;
};
//class Son:public Base//错误,必须要知道父类中的T类型,才能继承给子类
class Son:public Base
{};
template//要想灵活指定父类中的T类型,子类也需要变类模板
class Son2:public Base
{
    T1 obj;
};
int main()
{
    Son2S2;
    system("pause");
    return 0;
}

6.类模板成员函数类外实现

template
class Base
{
public:
    Base(T1 name,T2 age);
    void showPerson();
    T1 m_name;
    T2 m_age;
};
template
Base::Base(T1 name,T2 age)//构造函数类外实现
{
    this->m_name = name;
    this->m_age = age;
}
template
void Base::showPerson()//成员函数类外实现
{
    cout << "\"Baseshow\" name is " << m_name << " and age is " << m_age<< endl;
}
void test()
{
    BaseObj("Stan",22);
    Obj.showPerson();
}
int main()
{
    test();
    system("pause");
    return 0;
}

类模板中成员函数类外实现时,需要加上模板参数列表

7.类模板分文件编写

问题:

  • 类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到

解决:

  • 解决方式1:直接包含.cpp源文件

  • 解决方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制(主流解决方法)

头文件person.h

#pragma once
#include 
#include 
using namespace std;

template
class Person
{
public:
    Person(T1 name,T2 age);
    void showPerson();
    T1 m Name;
    T2 m Age;
};

源文件person.cpp

#include "person.h"
template 
Person : Person(T1 name, T2 age)
{
    this->m_Name = name;
    this->m_Age = age:
}
template 
void Person : showPerson()
{
    cout << "name is " << this->m_Name << "; age is " << this->mAge << endl;
}

主文件main.cpp

//#include "person.h"这么引用会报错,因为只会在调用的时候创建,无法解析外部命令
#include "person.cpp"//直接包含.cpp文件就可以解决
void test()
{
    Person p("Jerry", 18);
    p.showPerson();
}

合并person.h person.cpp文件为person.hpp,同时main.cpp主文件要改#include “person.cpp”为#include “person.hpp”即可

#pragma once
#include 
#include 
using namespace std;

template
class Person
{
public:
    Person(T1 name,T2 age);
    void showPerson();
    T1 m Name;
    T2 m Age;
};
template 
Person :: Person(T1 name, T2 age)
{
    this->m_Name = name;
    this->m_Age = age:
}
template 
void Person :: showPerson()
{
    cout << "name is " << this->m_Name << "; age is " << this->m_Age << endl;
}

8.类模板与友元

  • 全局函数类内实现——直接在类内声明友元即可

  • 全局函数类外实现——需要提前让编译器知道全局函数的存在

template
class Person
{
    friend void printPerson(Personp)//全局函数类内实现
    {
        cout<<"name is"<m_Name = name;
        this->m_Age = age;
    }
private:
    T1 m_Name;
    T2 m_Age;
};

类外实现:

template
class Person;//因为类外实现的模板函数用到了Person类,需要让编译器知道类的存在

template//需要让编译器知道函数的存在
void class_out_printPerson(Personp)// 全局函数类外实现
{
    cout << "name is" << p.m_Name << " age is " << p.m_Age << endl;
}

template 
class Person
{
    //friend void class_out_printPerson(Personp);这是一个普通函数的声明,但是实现却是模板函数为了匹配
    friend void class_out_printPerson<>(Personp);//需要加一个 空模板 参数列表
public:
    Person(T1 name, T2 age)
    {
        this->m_Name = name;
        this->m_Age = age;
    }
private:
    T1 m_Name;
    T2 m_Age;
};

int main()
{
    system("pause");
    return 0;
}

建议全局做类内实现,用法简单,而且编译器可以直接识别

十五、STL

1、STL初识

  • 长久以来,软件界一直希望建立一种可重复利用的东西

  • C++的面向对象泛型编程思想,目的就是复用性的提升

  • 大多情况下,数据结构和算法都未能有一套标准,导致被迫从事大量重复工作

  • 为了建立数据结构和算法的一套标准,诞生了STL

1.概念

  • STL(Standard Template Library,标准模板库)

  • STL从广义上分为:容器(container)算法(algorithm)迭代器(iterator)

  • 容器和算法之间通过迭代器进行无缝连接。

  • STL几乎所有的代码都采用了模板类或者模板函数

2、STL-常用容器

3、STL-函数对象

4、STL-常用算法

十六、其他

1、左值和右值

首先,让我们避开那些正式的定义。在C++中,一个左值是指向一个指定内存的东西。另一方面,右值就是不指向任何地方的东西。通常来说,右值是暂时和短命的,而左值则活的很久,因为他们以变量的形式(variable)存在。我们可以将左值看作为容器(container)而将右值看做容器中的事物。如果容器消失了,容器中的事物也就自然就无法存在了。
让我们现在来看一些例子:

int x = 666; //ok

在这里,666是一个右值。一个数字(从技术角度来说他是一个字面常量(literal constant))没有指定的内存地址,当然在程序运行时一些临时的寄存器除外。在该例中,666被赋值(assign)给xx是一个变量。一个变量有着具体(specific)的内存位置,所以他是一个左值。C++中声明一个赋值(assignment)需要一个左值作为它的左操作数(left operand):这完全合法。
对于左值x,你可以做像这样的操作:

int* y = &x;  //ok

在这里我通过取地址操作符&获取了x的内存地址并且把它放进了y&操作符需要一个左值并且产生了一个右值,这也是另一个完全合法的操作:在赋值操作符的左边我们有一个左值(一个变量),在右边我们使用取地址操作符产生的右值。
然而,我们不能这样写:

int y;
666 = y; //error!

可能上面的结论是显而易见的,但是从技术上来说是因为666是一个字面常量也就是一个右值,它没有一个具体的内存位置(memory location),所以我们会把y分配到一个不存在的地方。
下面是GCC给出的变异错误提示:

error: lvalue required as left operand of assignment

赋值的左操作数需要一个左值,这里我们使用了一个右值666
我们也不能这样做:

int* y = &666;//   error~

GCC给出了以下错误提示:

error: lvalue required as unary ‘&’ operand`

&操作符需要一个左值作为操作数,因为只有一个左值才拥有地址。

1、返回左值和右值的函数

我们知道一个赋值的左操作数必须是一个左值,因此下面的这个函数肯定会抛出错误:lvalue required as left operand of assignment

int setValue()
{
    return 6;
}

// ... somewhere in main() ...

setValue() = 3; // error!

错误原因很清楚:setValue()返回了一个右值(一个临时值6),他不能作为一个赋值的左操作数。现在,我们看看如果函数返回一个左值,这样的赋值会发生什么变化。看下面的代码片段(snippet):

int global = 100;

int& setGlobal()
{
    return global;    
}

// ... somewhere in main() ...

setGlobal() = 400; // OK

该程序可以运行,因为在这里setGlobal()返回一个引用(reference),跟之前的setValue()不同。一个引用是指向一个已经存在的内存位置(global变量)的东西,因此它是一个左值,所以它能被赋值。注意这里的&:它不是取地址操作符,他定义了返回的类型(一个引用)。
可以从函数返回左值看上去有些隐晦,它在你做一些进阶的编程例如实现一些操作符的重载(implementing overload operators)时会很有作用,这些知识会在未来的章节中讲述。

2、左值到右值的转换

一个左值可以被转换(convert)为右值,这完全合法且经常发生。让我们先用+操作符作为一个例子,根据C++的规范(specification),它使用两个右值作为参数并返回一个右值(译者按:可以将操作符理解为一个函数)。
让我们看下面的代码片段:

int x = 1;
int y = 3;
int z = x + y;   // ok

等一下,xy是左值,但是加法操作符需要右值作为参数:发生了什么?答案很简单:xy经历了一个隐式(implicit)的左值到右值(lvalue-to-rvalue)的转换。许多其他的操作符也有同样的转换——减法、加法、除法等等。

3、左值引用

相反呢?一个右值可以被转化为左值吗?不可以,它不是技术所限,而是C++编程语言就是那样设计的。
在C++中,当你做这样的事:

int y = 10;
int& yref = y;
yref++;        // y is now 11

这里将yref声明为类型int&:一个对y的引用,它被称作左值引用(lvalue reference)。现在你可以开心地通过该引用改变y的值了。
我们知道,一个引用必须只想一个具体的内存位置中的一个已经存在的对象,即一个左值。这里y确实存在,所以代码运行完美。
现在,如果我缩短整个过程,尝试将10直接赋值给我的引用,并且没有任何对象持有该引用,将会发生什么?

int& yref = 10;  // will it work?

在右边我们有一个临时值,一个需要被存储在一个左值中的右值。在左边我们有一个引用(一个左值),他应该指向一个已经存在的对象。但是10 是一个数字常量(numeric constant),也就是一个左值,将它赋给引用与引用所表述的精神冲突。
如果你仔细想想,那就是被禁止的从右值到左值的转换。一个volitile的数字常量(右值)如果想要被引用,需要先变成一个左值。如果那被允许,你就可以通过它的引用来改变数字常量的值。相当没有意义,不是吗?更重要的是,一旦这些值不再存在这些引用该指向哪里呢?
下面的代码片段同样会发生错误,原因跟刚才的一样:

void fnc(int& x)
{
}

int main()
{
    fnc(10);  // Nope!
    // This works instead:
    // int x = 10;
    // fnc(x);
}

我将一个临时值10传入了一个需要引用作为参数的函数中,产生了将右值转换为左值的错误。这里有一个解决方法(workaround),创造一个临时的变量来存储右值,然后将变量传入函数中(就像注释中写的那样)。将一个数字传入一个函数确实不太方便。

4、常量左值引用

先看看GCC对于之前两个代码片段给出的错误提示:

error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’

GCC认为引用不是const的,即一个常量。根据C++规范,你可以将一个const的左值绑定到一个右值上,所以下面的代码可以成功运行:

const int& ref = 10;  // OK!

当然,下面的也是:

void fnc(const int& x)
{
}

int main()
{
    fnc(10);  // OK!
}

背后的道理是相当直接的,字面常量10volatile的并且会很快失效(expire),所以给他一个引用是没什么意义的。如果我们让引用本身变成常量引用,那样的话该引用指向的值就不能被改变了。现在右值被修改的问题被很好地解决了。同样,这不是一个技术限制,而是C ++人员为避免愚蠢麻烦所作的选择。

应用:C++中经常通过常量引用来将值传入函数中,这避免了不必要的临时对象的创建和拷贝。
编译器会为你创建一个隐藏的变量(即一个左值)来存储初始的字面常量,然后将隐藏的变量绑定到你的引用上去。那跟我之前的一组代码片段中手动完成的是一码事,例如:

// the following...
const int& ref = 10;

// ... would translate to:
int __internal_unique_name = 10;
const int& ref = __internal_unique_name;

现在你的引用指向了真实存在的事物(知道它走出作用域外)并且你可以正常使用它,出克改变他指向的值。

const int& ref = 10;
std::cout << ref << "\n";   // OK!
std::cout << ++ref << "\n"; // error: increment of read-only reference ‘ref’

2、lambda表达式

lambda表达式有如下优点:

  • 声明式编程风格:就地匿名定义目标函数或函数对象,不需要额外写一个命名函数或者函数对象。以更直接的方式去写程序,好的可读性和可维护性。
  • 简洁:不需要额外再写一个函数或者函数对象,避免了代码膨胀和功能分散,让开发者更加集中精力在手边的问题,同时也获取了更高的生产率。
  • 在需要的时间和地点实现功能闭包,使程序更灵活。

lambda 表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda 表达式的语法形式可简单归纳如下:

[ capture ] ( params ) opt -> ret { body; };

其中 capture 是捕获列表,params 是参数表,opt 是函数选项,ret 是返回值类型,body是函数体。

因此,一个完整的 lambda 表达式看起来像这样:

auto f = [](int a) -> int { return a + 1; };//定义了返回值为int
std::cout << f(1) << std::endl;  // 输出: 2

可以看到,上面通过一行代码定义了一个小小的功能闭包,用来将输入加 1 并返回。

在 C++11 中,lambda 表达式的返回值是通过前面介绍的C++返回值类型后置,语法来定义的。其实很多时候,lambda 表达式的返回值是非常明显的,比如这个例子。因此,C++11 中允许省略 lambda 表达式的返回值定义:

auto f = [](int a){ return a + 1; };

这样编译器就会根据 return 语句自动推导出返回值类型。

需要注意的是,初始化列表不能用于返回值的自动推导:

auto x1 = [](int i){ return i; }; // OK: return type is int
auto x2 = [](){ return { 1, 2 }; }; // error: 无法推导出返回值类型

这时我们需要显式给出具体的返回值类型

另外,lambda 表达式在没有参数列表时,参数列表是可以省略的。因此像下面的写法都是正确的:

auto f1 = [](){ return 1; };
auto f2 = []{ return 1; }; // 省略空参数表

1.使用 lambda 表达式捕获列表

lambda 表达式还可以通过捕获列表捕获一定范围内的变量:

  • [] 不捕获任何变量
  • [&] 捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)
  • [=] 捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)
  • [=,&foo] 按值捕获外部作用域中所有变量,并按引用捕获 foo 变量。
  • [bar] 按值捕获 bar 变量,同时不捕获其他变量
  • [this] 捕获当前类中的 this 指针,让 lambda 表达式拥有和当前类成员函数同样的访问权限。如果已经使用了 & 或者 =,就默认添加此选项。捕获 this 的目的是可以在 lamda 中使用当前类的成员函数和成员变量

【实例】lambda 表达式的基本用法。

class A{
public:    
    int i_ = 0;
    void func(int x, int y)
    {
        auto x1 = []{ return i_; };                    // error,没有捕获外部变量
        auto x2 = [=]{ return i_ + x + y; };           // OK,捕获所有外部变量
        auto x3 = [&]{ return i_ + x + y; };           // OK,捕获所有外部变量
        auto x4 = [this]{ return i_; };                // OK,捕获this指针        
        auto x5 = [this]{ return i_ + x + y; };        // error,没有捕获x、y        
        auto x6 = [this, x, y]{ return i_ + x + y; };  // OK,捕获this指针、x、y        
        auto x7 = [this]{ return i_++; };              // OK,捕获this指针,并修改成员的值    
    }};
int a = 0, b = 1;
auto f1 = []{ return a; };               // error,没有捕获外部变量
auto f2 = [&]{ return a++; };            // OK,捕获所有外部变量,并对a执行自加运算
auto f3 = [=]{ return a; };              // OK,捕获所有外部变量,并返回a
auto f4 = [=]{ return a++; };            // error,a是以复制方式捕获的,无法修改
auto f5 = [a]{ return a + b; };          // error,没有捕获变量b
auto f6 = [a, &b]{ return a + (b++); };  // OK,捕获a和b的引用,并对b做自加运算
auto f7 = [=, &b]{ return a + (b++); };  // OK,捕获所有外部变量和b的引用,并对b做自加运算

从上例中可以看到,lambda 表达式的捕获列表精细地控制了 lambda 表达式能够访问的外部变量,以及如何访问这些变量。

需要注意的是,默认状态下 lambda 表达式无法修改通过复制方式捕获的外部变量。如果希望修改这些变量的话,我们需要使用引用方式进行捕获。

一个容易出错的细节是关于 lambda 表达式的延迟调用的:

int a = 0;
auto f = [=]{ return a; };      // 按值捕获外部变量a += 1;
                                // a被修改了
std::cout << f() << std::endl;  // 输出?

在这个例子中,lambda 表达式按值捕获了所有外部变量。在捕获的一瞬间,a 的值就已经被复制到f中了。之后 a 被修改,但此时 f 中存储的 a 仍然还是捕获时的值,因此,最终输出结果是 0。

如果希望 lambda 表达式在调用时能够即时访问外部变量,我们应当使用引用方式捕获。

从上面的例子中我们知道,按值捕获得到的外部变量值是在 lambda 表达式定义时的值。此时所有外部变量均被复制了一份存储在 lambda 表达式变量中。此时虽然修改 lambda 表达式中的这些外部变量并不会真正影响到外部,我们却仍然无法修改它们。

那么如果希望去修改按值捕获的外部变量应当怎么办呢?这时,需要显式指明 lambda 表达式为 mutable:

int a = 0;auto f1 = [=]{ return a++; };             // error,修改按值捕获的外部变量
auto f2 = [=]() mutable { return a++; };  // OK,= + mutable关键字也可以被修改

需要注意的一点是,被 mutable 修饰的 lambda 表达式就算没有参数也要写明参数列表。

2.lambda 表达式的类型

最后,介绍一下 lambda 表达式的类型。

lambda 表达式的类型在 C++11 中被称为“闭包类型(Closure Type)”。它是一个特殊的,匿名的非 nunion 的类类型。

因此,我们可以认为它是一个带有 operator() 的类,即仿函数。因此,我们可以使用 std::function 和 std::bind 来存储和操作 lambda 表达式:

std::function  f1 = [](int a){ return a; };
std::function f2 = std::bind([](int a){ return a; }, 123);

另外,对于没有捕获任何变量的 lambda 表达式,还可以被转换成一个普通的函数指针:

using func_t = int(*)(int);
func_t f = [](int a){ return a; };f(123);

lambda 表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量,最终均会变为闭包类型的成员变量。而一个使用了成员变量的类的 operator(),如果能直接被转换为普通的函数指针,那么 lambda 表达式本身的 this 指针就丢失掉了。而没有捕获任何外部变量的 lambda 表达式则不存在这个问题。

这里也可以很自然地解释为何按值捕获无法修改捕获的外部变量。因为按照 C++ 标准,lambda 表达式的 operator() 默认是 const 的。一个 const 成员函数是无法修改成员变量的值的。而 mutable 的作用,就在于取消 operator() 的 const。

需要注意的是,没有捕获变量的 lambda 表达式可以直接转换为函数指针,而捕获变量的 lambda 表达式则不能转换为函数指针。看看下面的代码:

typedef void(*Ptr)(int*);
Ptr p = [](int* p){delete p;};  // 正确,没有状态的lambda(没有捕获)的lambda表达式可以直接转换为函数指针
Ptr p1 = [&](int* p){delete p;};  // 错误,有状态的lambda不能直接转换为函数指针

上面第二行代码能编译通过,而第三行代码不能编译通过,因为第三行的代码捕获了变量,不能直接转换为函数指针。

3.声明式的编程风格,简洁的代码

就地定义匿名函数,不再需要定义函数对象,大大简化了标准库算法的调用。比如,在 C++11 之前,我们要调用 for_each 函数将 vector 中的偶数打印出来,如下所示。

【实例】lambda 表达式代替函数对象的示例。

class CountEven{    
    int& count_;
public:    
    CountEven(int& count) : count_(count) {}
    void operator()(int val)
    {        
        if (!(val & 1))       // val % 2 == 0        
        {
            ++ count_;        
        }    
}};
std::vector v = { 1, 2, 3, 4, 5, 6 };
int even_count = 0;
for_each(v.begin(), v.end(), CountEven(even_count));
std::cout << "The number of even is " << even_count << std::endl;

这样写既烦琐又容易出错。有了 lambda 表达式以后,我们可以使用真正的闭包概念来替换掉这里的仿函数,代码如下:

std::vector v = { 1, 2, 3, 4, 5, 6 };
int even_count = 0;
for_each( v.begin(), v.end(), [&even_count](int val)
{            
    if (!(val & 1))  // val % 2 == 0            
    {++ even_count;}        
});
    std::cout << "The number of even is " << even_count << std::endl;

lambda 表达式的价值在于,就地封装短小的功能闭包,可以极其方便地表达出我们希望执行的具体操作,并让上下文结合得更加紧密。


文章作者: Nico
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Nico !
  目录