打怪之旅-C++基础

小tips

  1. volatile

    • volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
  • volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
    • const 可以是 volatile (如只读的状态寄存器)
    • 指针可以是 volatile
  1. 常量:用于记录程序中不可更改的数据

    宏常量(声明没有分号)

    1
    # define name value

    const 修饰变量为常量

    1
    const int a = 1;
  2. 标识符:不能是关键字,区分大小写,只由字母\数字\下划线组成,首字符不能为数字.

  3. VS越界,便%(范围);

    1
    short a = 2^15 ; 会输出-2^15 ,因为short 的范围是[-2^15 , 2^15-1]
  4. sizeof 求数据类型占用内存大小

    1
    2
    默认情况下,只会显示6位小数
    float a = 3.1415926f //f是避免从double转换为float
  5. 转义字符:表示不能显示出来的ASCII字符

    1
    \n \t(8个位置,水平制表符,对齐效果)  \\(反斜杠)
  6. 取余\除 除数不能为0 , int/int = int, 小数不能做取模运算

  7. C++中除了0, 均为真 (-2也是真哦)

  8. 在C++中 三目运算符返回若返回的是变量, 可继续赋值

    1
    (a > b ? a : b) = 100   //a,b中大的那个赋值为100
  9. C++随机种子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //随机数发生器----产生伪随机数字(是由小M多项式序列生成的,其中产生每个小序列都有一个初始值,即随机种子。(注意: 小M多项式序列的周期是65535,即每次利用一个随机种子生成的随机数的周期是65535,当你取得65535个随机数后它们又重复出现了。))
    //线性同余法 是根据一个数(我们可以称它为种子)为基准以某个递推公式推算出来的一系列数,当这系列数很大的时候,就符合正态公布,从而相当于产生了随机数,但这不是真正的随机数,当计算机正常开机后,这个种子的值是定了的,除非你破坏了系统。
    所在头文件: stdlib.h (被包含于iostream中)
    int rand(void)

    //初始化随机数发生器, 设置rand()产生随机数时的随机数种子。参数seed必须是个整数,如果每次seed都设相同值,rand()所产生的随机数值每次就会一样。 缺省值为1
    void srand(unsigned int seed)

    //使用当前时钟作为随机数种子
    //time函数可以获取当前的系统时间(但是获取的是秒数,是从1970年1月1日零时零分零秒到目前为止所经过的时间),ctime可以将其转化为常规的时间
    在c中的头文件为<time.h>,在c++中头文件为<ctime>
    原型为: time_t time(time_t * timer) (其中time_tlong int)
    char * ctime(const time_t *time)
    srand((unsigned int)time(NULL));

    线性同余方法

    线性同余方法(LCG)是一种产生伪随机数的方法。

    它是根据递归公式:RandSeed = (A * RandSeed + B) % M

    线性同余法最重要的是定义了三个整数,乘数 A、增量 B和模数 M,其中A, B, M是产生器设定的常数。 LCG的周期最大为 M,但大部分情况都会少于M。要令LCG达到最大周期,应符合以下条件:

    1. B,M互质;

    2. M的所有质因数都能整除A-1;

    3. 若M是4的倍数,A-1也是;

    4. A,B,N[0]都比M小;

    5. A,B是正整数。

  1. goto无条件跳转

    1
    2
    3
    goto flag;
    flag:
    .....
  2. 1
    二维数组初始化:  数组[][列数]  = {a,b,c,...}
  3. 函数的声明–告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。

    函数的声明可以多次,但是函数的定义只能有一次, 在.h头文件中写函数的声明,在.cpp源文件中写函数的定义

  4. #include<> 和 #include””的区别

    1
    2
    3
    4
    5
    6
    7
    #include< file >编译程序会先到   标准函数库  中找文件, 指示预处理程序到预定义的缺省路径下寻找文件。预定义的缺省路径通常是在INCLUDE环境变量中指定的.  一般用来包含标准头文件(例如stdio.h或stdlib.h),因为这些头文件极少被修改,并且它们总是存放在编译程序的标准包含文件目录下。

    #include”file” 编译程序会先从 当前目录 中找文件,再到预定义的缺省路径下寻找文件. 一般用来包含非标准头文件,因为这些头文件一般存放在当前目录下,可以经常修改它们,并且要求编译程序总是使用这些头文件的最新版本。

    例:INCLUDE=C:\COMPILER\INCLUDE;S:\SOURCE\HEADERS;
    1, 如果用#include<file >语句包含文件,编译程序将首先到C:\COMPILER\INCLUDE目录下寻找文件;如果未找到,则到S:\SOURCE\HEADERS目录下继续寻找;如果还未找到,则到当前目录下继续寻找。
    2, 如果用#include“file”语句包含文件,编译程序将首先到当前目录下寻找文件;如果未找到,则到C:\COMPILER\INCLUDE目录下继续寻找;如果还未找到,则到S:\SOURCE\HEADERS目录下继续寻找。
  5. 在32位(编译环境)中,指针(无论什么类型int,char,double…)都占4个字节; 64位,则占8个字节.

    在32位系统和64位系统(32,64指的是寄存器的位宽)下只有指针类型和长整型字节数有所差别,其余全部相同

    image-20210317172838180

  6. 空指针与野指针

    空指针:指针变量指向内存中编号为0的空间,初始化指针变量

    (悬挂指针)野指针:指针变量指向非法的内存空间

    内存编号0 ~255为系统占用内存,不允许用户访问,空指针和野指针都不是我们申请的空间,因此不要访问。

  7. const 修饰指针

    1
    2
    3
    4
    5
    6
    指针和const出现的顺序
    常量指针
    const int *p = &a; (*p 不能修改值) 关键是cons离(* / p ) 谁近

    指针常量
    int * const p = &a; (p 不能修改地址)
  8. 指针和数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    核心  优先级:()>[]>*
    主要看后面的两个字是什么(前面是修饰作用),因此指针数组是数组,而数组指针是指针。
    //数组指针(也称行指针)-是指一个指向数组的指针,执行p+1时,p要跨过n个整型数据的长度。
    定义形式:int (*p)[n];

    eg:
    如要将二维数组赋给一指针,应这样赋值:
    int a[3][4];
    int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。
    p=a; //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
    p++; //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]

    //指针数组-是指一个数组里面装着指针,也即指针数组是一个数组;有n个指针类型的数组元素
    定义形式:int *p[n];

    //a 和&a 之间的区别
    &a 是整个数组的首地址,a是数组首元素(单个字符)的首地址,其值相同但意义不同。
    报错如下,
    char (*p2)[5]=a;必须使用强制转换,如:char (*p2)[5]=(char (*)[5])a; 并且大小必须一致.

    image-20210318114634784

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    指针变量与一个整数相加减并不是用指针变量里的地址直接加减这个整数。这个整数的单位不是byte 而是元素的个数;
    eg:
    struct Test
    {
    int Num;
    char *pcName;
    short sDate;
    char cha[2];
    short sBa[4];
    }*p;

    假设p 的值为0x100000。如下表表达式的值分别为多少?
    p + 0x1 = 0x___ ?
    //0x100000+sizof(Test)*0x1

    (unsigned long)p + 0x1 = 0x___?
    //涉及到强制转换,将指针变量p 保存的值强制转换成无符号的长整型数。任何数值一旦被强制转换,其类型就改变了。所以这个表达式其实就是一个无符号的长整型数加上另一个整数。所以其值为:0x100001。

    (unsigned int*)p + 0x1 = 0x___?
    //p 被强制转换成一个指向无符号整型的指针。所以其值为:0x100000+sizof(unsigned int)*0x1,等于0x100004
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <<C语言深度剖析>>陈正冲 著
    int main()
    {
    int a[5]={1,2,3,4,5};
    int *ptr1=(int*)(&a+1);
    int *ptr2=(int*)((int)a+1);
    printf("%x,%x",ptr1[-1],*ptr2);
    return 0;
    }

    这里讲的比较清楚:

    https://www.cnblogs.com/mq0036/p/3382732.html

    https://zhidao.baidu.com/question/521307615.html

  9. 大端模式与小端模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    大端模式:高 位字节排放在内存的低 地址端,低位字节排放在内存的高地址端。
    小端模式:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

    //大端小端没有谁优谁劣,各自优势便是对方劣势:
    小端模式 :强制转换数据不需要调整字节内容,124字节的存储方式一样。
    大端模式 :符号位的判定固定为第一个字节,容易判断正负。

    int checkSystem()
    {
    union check
    {
    int i;
    char ch;
    } c;
    c.i = 1;
    return (c.ch ==1);//如果当前系统为大端模式这个函数返回0;如果为小端模式,函数返回1。
    }
  10. 结构体 struct

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct 结构体名 { 结构体成员列表 };
    创建变量方式:
    1 struct student
    {
    //成员列表
    string name;
    int age;
    }stu3; //结构体变量直接创建
    2 struct student stu1; //struct 关键字可以省略 .成员在初始化
    3 struct student stu2 = { "李四",19 };

    student *p = &stu1;
    p->name
  11. 共用体 union

    同一时刻,共用体只存放一个被选中的成员,对不同成员的赋值会导致对其他成员重写(原来成员的值就不存在了)—因此可以用于判断大小端模式

    • 默认访问控制符为 public
    • 可以含有构造函数、析构函数
    • 不能含有引用类型的成员
    • 不能继承自其他类,不能作为基类
    • 不能含有虚函数
    • 匿名 union 在定义所在作用域可直接访问 union 成员
    • 匿名 union 不能包含 protected 成员或 private 成员
    • 全局匿名联合必须是静态(static)的
  12. 字节对齐(结构体、共用体):1,占用的内存空间大小需要是结构体中占用最大内存空间的类型的整数倍。 2,内存起始地址要是数据类型的整数倍

  13. #pragma once 指定该文件在编译源代码文件时仅由编译器包含(打开)一次。使用预处理宏定义#ifndef HEADER_H_ .....#define HEADER_H_来避免多次包含文件的内容的效果是一样的,但是需要键入的代码少,可减少错误率

  14. 对于内置类型而言,new仅仅是分配内存,除非后面显示加(),相当于调用它的构造函数,对于自定义类型而言,只要一调用new,那么编译器不仅仅给它分配内存,还调用它的默认构造函数初始化,即使后面没有加()

  15. 函数外定义则为全局变量,系统初始化为0;函数内定义则为局部变量,是随机值,局部变量需要初始化才能使用

  16. 宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对 “参数” 进行的是一对一的替换。

  17. inline 内联函数

    程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。

    特征

    • 相当于把内联函数里面的内容写在调用内联函数处
    • 相当于不用执行进入函数的步骤,直接执行函数体;
    • 相当于宏,却比宏多了类型检查,真正具有函数特性;
    • 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
    • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数

优点

  • 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
  • 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
  • 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
  • 内联函数在运行时可调试,而宏定义不可以。

缺点

  • 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  • inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
    是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。

虚函数(virtual)可以是内联函数(inline)吗?

虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联
内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

内存分区模型

C++程序在执行时,将内存大方向划分为4个区域

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

内存四区意义:不同区域存放的数据,赋予不同的生命周期, 给我们更大的灵活编程

程序运行前

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

代码区:

​ 存放 CPU 执行的机器指令

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

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

全局区:

全局变量和静态变量存放在此.

​ 全局区还包含了常量区, 字符串常量和其他常量( const修饰的全局常量)也存放在此.

​ ==该区域的数据在程序结束后由操作系统释放==.

程序运行后

栈区:

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

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

堆区:

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

​ 在C++中主要利用new在堆区开辟内存(返回的是地址,一般用指针接收),delete释放内存.

引用

给变量起别名,指向同一块地址.

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

  • 引用必须初始化,初始化后,不可以更改地址(赋值不是更改)
  • 引用只能绑定在已存在变量上,不能与字面值或某个表达式的计算结果绑定在一起
  • 指针是解引用(按地址传递),引用参数产生的效果同按地址传递.
  • 不要返回局部变量引用,因为局部变量生命周期结束后由编译器释放了.
  • 函数做左值,那么必须返回引用,多用于链式操作p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
  • 常量引用(可以绑定常量)(相当于const即修饰指针又修饰常量) -> 修饰形参,防止形参(eg:const int& v)改变实参

函数参数

默认参数

  • 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
  • 声明与定义之中,只能有一个有默认参数

占位参数

1
2
3
4
5
6
7
8
9
10
//函数占位参数 ,占位参数也可以有默认参数
void func(int a, int) {
cout << "this is func" << endl;
}

int main() {
func(10,10); //占位参数必须填补
system("pause");
return 0;
}

# 函数重载

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

函数重载满足条件:

  • 同一个作用域下
  • 函数名称相同
  • 函数参数类型不同 或者 个数不同或者 顺序不同

注意:

  • 函数的返回值不可以作为函数重载的条件
  • 引用作为重载条件(若传入常量时,需用常量引用const int &a
  • 函数重载碰到函数默认参数(可能产生歧义,需要避免)

类和对象

封装

C++面向对象的三大特性为:封装、继承、多态

具有相同性质的对象(有许多属性和行为[术语:成员]),我们可以抽象称为类

成员被访问的权限:

  • 公共权限 public 类内可以访问 类外可以访问
  • 保护权限 protected 类内可以访问 类外不可以访问 [子类可以访问]
  • 私有权限 private 类内可以访问 类外不可以访问  [子类不可以访问]

在C++中 struct和class唯一的区别就在于 默认的访问权限不同

区别:

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

对象的初始化和清理

构造函数和析构函数

  • 构造函数类名(){}:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。

  • 析构函数~类名(){}:主要作用在于对象销毁前系统自动调用,执行一些清理工作。析构函数不可以有参数,因此不可以发生重载

    编译器提供的构造函数和析构函数是空实现。

构造函数分类

 按照参数分类分为 有参和无参构造 无参又称为默认构造函数
 按照类型分类分为 普通构造和拷贝构造Person(const Person& p){}

​ 调用方式:括号法\显示法\隐式转换法

注意:

  • 调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明

  • 不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明

    1
    2
    Person (p3);
    //编译器会认为 Person (p3) = Person p3;重定义p3了.

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

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

构造函数调用规则

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

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,对属性进行值拷贝

4.赋值运算符 operator=, 对属性进行值拷贝[易发生浅拷贝带来的重复释放堆区问题]

如果类中有属性指向堆区,默认拷贝构造函数\做赋值操作时 会出现深浅拷贝问题

构造函数调用规则如下:

  • 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
  • 当类中成员是其他类对象时,我们称该成员为 对象成员.构造的顺序是 :先调用对象成员的构造,再调用本类构造;析构顺序与构造相反.

深拷贝与浅拷贝

浅拷贝:简单的赋值拷贝操作(指向同一地址,易发生堆区内存重复释放)

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//拷贝构造函数  
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_age = p.m_age;
//m_height = p.m_heigh (浅拷贝,系统默认拷贝构造函数)  //m_height 是指针
m_height = new int(*p.m_height);
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
if (m_height != NULL)
{
//如果用的浅拷贝,p1 p2的m_height指针指向堆区同一块地址
// p2析构后,p1的m_height就变成了悬挂指针(野指针)
delete m_height;
}
}

静态成员

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

  • 静态成员变量(相当于全局变量
    • 所有对象共享同一份数据
    • 编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有对象共享同一个函数
    • C++规定静态成员函数没有this指针
    • 静态成员函数只能访问静态成员变量(因为静态成员在编译阶段就已经分配好内存了,非静态成员变量只有在实例化后才分配内存;还有一种解释是非静态成员变量独属于一个对象,无法确定静态成员函数访问的哪个对象的非静态成员变量)

C++对象模型和this指针

空对象

空对象占用内存空间为:1 为了区分空对象在内存的位置(占位置),独一无二的内存地址

在C++中,类内的成员变量和成员函数分开存储

只有 非静态成员变量 才属于类的对象上,如果存在虚函数,则要加上虚函数(表)指针,所以空对象占用内存空间变更为4(8是64位).

image-20210319114817848

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
public:
Person() {
mA = 0;
}
//非静态成员变量占对象空间
int mA;
//静态成员变量不占对象空间
static int mB;
//函数也不占对象空间,所有函数共享一个函数实例
void func() {
cout << "mA:" << this->mA << endl;
}
//静态成员函数也不占对象空间
static void sfunc() {
}
};

this指针

this指针指向 被调用的成员函数所属的 对象,this指针的本质是一个指针常量

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

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

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this
1
2
3
4
5
6
7
8
9
10
11
12
13
Person(int age)
{
//1、当形参和成员变量同名时,可用this指针来区分
this->age = age;
}

//注意这里的引用操作(返回对象本体),链式编程 [不加& 会自动调用拷贝构造函数,此时返回值为p的拷贝体]
Person& PersonAddPerson(Person p)
{
this->age += p.age;
//返回对象本身  this是指向p的指针, *this就是对象本体(p).
return *this;
}

空指针访问成员函数

空指针(对象未实例化)可以调用成员函数,但是函数里访问非静态成员就会出错(因为没有分配内存)

const修饰成员函数

常函数:

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

常对象:

  • 声明对象前加const称该对象为常对象

  • 常对象只能调用常函数

  • 非常量对象也可以调用常成员函数,但是如果有重载的非常成员函数(与常函数类似)则会调用非常成员函数。

const能否用于重载:

(1)常成员函数和非常成员函数之间的重载

(2)const修饰成员函数形参时的重载(形参类型本质上不能相同,否则会出现重定义)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Person {
public:
Person() {
m_A = 0;
m_B = 0;
}

//this指针的本质是一个指针常量,指针的指向不可修改
//如果想让指针指向的值也不可以修改,需要声明常函数
//此时 this 相等于 const Person * const this;
void ShowPerson() const {
this = NULL; //不能修改指针的指向
this->mA = 100; //this指针指向的对象的数据不可以修改的
//const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
this->m_B = 100;
}

void MyFunc() const {
//mA = 10000;
}

public:
int m_A;
mutable int m_B; //可修改 可变的
};


//const修饰对象 常对象
void test01() {
// Person person;
const Person person; //常量对象
cout << person.m_A << endl;
//person.mA = 100; //常对象不能修改成员变量的值,但是可以访问
person.m_B = 100; //但是常对象可以修改mutable修饰成员变量

//常对象访问成员函数
person.MyFunc(); //常对象不能调用const的函数

}

int main() {

test01();

system("pause");

return 0;
}

友元

让一个函数或者类A (写在类B内)访问另一个类B中私有成员

  • 全局函数做友元

    1
    2
    3
    4
    5
    class Building
    {
    //告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
    friend void goodGay(Building * building);
    }
  • 类做友元

    1
    2
    3
    4
    5
    class Building
    {
    //告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
    friend class goodGay;
    }
  • 成员函数做友元

    1
    2
    3
    4
    5
    class Building
    {
    //告诉编译器 goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
    friend void goodGay::visit();
    }

运算符重载

运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型

对于内置的数据类型的表达式的的运算符是不可能改变的,不要滥用运算符重载(意义相同,+就是+)

operator 关键字

加号运算符

1
2
3
4
5
6
7
8
9
//成员函数(放到类内)/全局函数(放到外面)实现 + 号运算符重载,可以发生函数重载(参数不同) 
Person operator+(const Person& p) {
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
Person p3 = p2 + p1; //相当于 p2.operaor+(p1) [成员函数]
Person p4 = p3 + 10; //相当于 operator+(p3,10) [全局函数]

左移运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//成员函数 实现不了我们想要的左移效果
#include <iostream>
#include<string>
using namespace std;
class test {
int a;
string b;
public:
test(int a,string b) {
this->a = a;
this->b = b;
}
ostream &operator<<(ostream &out) {
out << this->a << " " << this->b << endl; ;
return out;
}
};
int main() {
test t(1, "mrs .zhang");
t << cout<<endl;//这样才能正确的输出两个元素,颠覆了我们平时的调用习惯,
//希望是cout<<t<<endl;
}

//只能全局函数实现左移重载
//ostream对象只能有一个 ,所以用引用
ostream& operator<<(ostream& out, Person& p) {
out << "a:" << p.m_A << " b:" << p.m_B;
return out;
}
//本质上 operate<<(out, p )
cout << p1 << "hello world" << endl; //链式编程

递增运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//前置++, 注意返回引用,方便链式操作(套娃)
MyInteger& operator++() {
//先++
m_Num++;
//再返回
return *this;
}

//后置++ ,注意int[占位参数],用于区分前后置++ ; 而且不能返回局部变量temp的引用(地址),栈区开辟的数据由编译器自动释放,非法操作
MyInteger operator++(int) {
//先返回
MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
m_Num++;
return temp;
}

赋值运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//operator=必须是成员函数,因为要返回自身
class Person
{
public:
Person(int age)
{
//将年龄数据开辟到堆区
m_Age = new int(age);
}

//重载赋值运算符  又是返回本身[引用]
Person& operator=(Person &p)
{
//判断是否有本身属性在堆区,有就释放干净,再深拷贝(new)
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 test01()
{
Person p1(18);
Person p2(20);
Person p3(30);

p3 = p2 = p1; //赋值操作

cout << "p1的年龄为:" << *p1.m_Age << endl;
cout << "p2的年龄为:" << *p2.m_Age << endl;
cout << "p3的年龄为:" << *p3.m_Age << endl;
}

int main() {

test01();

//int a = 10;
//int b = 20;
//int c = 30;

//c = b = a;
//cout << "a = " << a << endl;
//cout << "b = " << b << endl;
//cout << "c = " << c << endl;
system("pause");
return 0;
}

关系运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
//!=     >=     <=
bool operator==(Person & p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return true;
}
else
{
return false;
}
}

函数调用运算符重载

  • 函数调用运算符 () 也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数(用于STL)
  • 仿函数没有固定写法,非常灵活
1
2
3
4
5
6
7
8
9
10
11
12
//operator()必须是成员函数,如果是全局函数,便会有二义性
void operator()(string text)
{
cout << text << endl;
}

int operator()(int v1, int v2)
{
return v1 + v2;
}
//匿名对象调用 ,该行运行完就被释放
cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;

继承

class 子类 : 继承方式 父类 eg:class A : public B;

A 类称为子类 或 派生类 [父类的私有成员不能继承,提高访问权限]

B 类称为父类 或 基类

父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到

查看对象模型的工具---'vs2017开发人员命令提示工具'到.cpp文件的目录, cl /d1 reportSingleClassLayout查看的类名 所属文件名

clip_image002

构造和析构顺序and同名成员处理方式

  • 继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
  • 访问子类同名成员(静态\非静态都一样) 直接访问即可,访问父类同名成员-需要加作用域

多继承语法

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

语法:class 子类 :继承方式 父类1 , 继承方式 父类2...

多继承可能会引发父类中有同名成员出现,需要加作用域区分

C++实际开发中不建议用多继承

菱形继承

​ 两个派生类继承同一个基类

​ 又有某个类同时继承者两个派生类

​ 这种继承被称为菱形继承,或者钻石继承.

问题 -- 利用虚继承解决:

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性

  2. 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

clip_image002

1
2
3
4
5
6
7
8
9
10
11
12
class Animal
{
public:
int m_Age;
};

//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
//这样 SheepTuo所继承的age成员只有一份,避免二义性

底层实现-虚指针

image-20210319110838668

多态

多态的优点:

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护

类别

多态分为两类

  • 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
  • 动态多态: 派生类和虚函数实现运行时多态

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

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

动态多态

动态多态满足条件:

1,有继承关系

2,派生类重写(函数返回值类型,函数名,参数列表完全相同)基类的虚函数

多态使用:父类指针或引用指向子类对象

原理分析

image-20210319114817848

使用工具查看

image-20210319115428239

cat没有重写speak()函数时,虚函数指针指向基类虚函数

image-20210319115652532

重写后

image-20210319115836252

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

C++开发提倡利用多态设计程序架构

纯虚函数和抽象类

虚函数改写为纯虚函数 virtual 返回值类型 函数名 (参数列表)= 0 ; =0是说明符

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

抽象类特点

  • 无法实例化抽象类对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

虚析构和纯虚析构

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

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

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现[纯虚析构函数需要类内声明,类外实现]

虚析构和纯虚析构区别:

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

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;

类名::~类名(){}

文件

文件类型分为两种:

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

操作文件的三大类:

  1. ofstream:写操作
  2. ifstream: 读操作
  3. fstream : 读写操作

写文件

步骤如下:

  1. 包含头文件

    #include <fstream>

  2. 创建流对象

    ofstream ofs;

  3. 打开文件

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

  4. 写数据

    ofs << “写入的数据”;

  5. 关闭文件

    ofs.close();

文件打开方式:

打开方式 解释
ios::in 为读文件而打开文件
ios::out 为写文件而打开文件
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在先删除,再创建
ios::binary 二进制方式

注意: 文件打开方式可以配合使用,利用|操作符

例如:用二进制方式写文件 ios::binary | ios:: out

读文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <fstream>
#include <string>
void test01()
{
ifstream ifs;
ifs.open("test.txt", ios::in);

if (!ifs.is_open())
{
cout << "文件打开失败" << endl;
return;
}

//第一种方式 字符数组
//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;
}

ifs.close();


}

int main() {

test01();

system("pause");

return 0;
}

二进制文件

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <fstream>
//2、创建输出流对象
ofstream ofs("person.txt", ios::out | ios::binary);

//3、打开文件
//ofs.open("person.txt", ios::out | ios::binary);

Person p = {"张三" , 18};

//4、写文件
ofs.write((const char *)&p, sizeof(p));

//5、关闭文件
ofs.close();

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

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

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

1
2
3
4
5
6
7
8
9
10
#include <fstream>
ifstream ifs("person.txt", ios::in | ios::binary);
if (!ifs.is_open())
{
cout << "文件打开失败" << endl;
}

Person p;
ifs.read((char *)&p, sizeof(p));
ofs.close();

模板

泛型编程

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

函数模板

语法:

1
2
template<typename T>
函数声明或定义

解释:

template — 声明创建模板

typename — 表面其后面的符号是一种数据类型,可以用class代替

T — 通用的数据类型,名称可以替换,通常为大写字母

总结:

  • 函数模板利用关键字 template
  • 使用函数模板有两种方式:自动类型推导、显示指定类型
  • 模板的目的是为了提高复用性,将类型参数化

注意事项:

  • 自动类型推导,必须推导出一致的数据类型T,才可以使用
  • 模板必须要确定出T的数据类型,才可以使用

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//交换整型函数
void swapInt(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}

//交换浮点型函数
void swapDouble(double& a, double& b) {
double temp = a;
a = b;
b = temp;
}

//利用模板提供通用的交换函数
template<typename T> //typename 可以替换成class
void mySwap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}

void test01()
{
int a = 10;
int b = 20;

//swapInt(a, b);

//利用模板实现交换
//1、自动类型推导
mySwap(a, b);

//2、显示指定类型
mySwap<int>(a, b);

cout << "a = " << a << endl;
cout << "b = " << b << endl;

}

int main() {

test01();

system("pause");

return 0;
}

普通函数与函数模板区别

  • 普通函数调用时可以发生自动类型转换(隐式类型转换
  • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  • 如果利用显示指定类型的方式,可以发生隐式类型转换

调用规则

  1. 如果函数模板和普通函数都可以实现,优先调用普通函数
  2. 可以通过空模板参数列表来强制调用函数模板myPrint<>(a, b); //调用函数模板
  3. 函数模板也可以发生重载
  4. 如果函数模板可以产生更好的匹配,优先调用函数模板

既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性

局限性

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

例如:

1
2
3
4
5
template<class T>
void f(T a, T b)
{
a = b;
}

在上述代码中提供的赋值操作,如果传入的a和b是一个数组,就无法实现了

再例如:

1
2
3
4
5
template<class T>
void f(T a, T b)
{
if(a > b) { ... }
}

在上述代码中,如果T的数据类型传入的是像Person这样的自定义数据类型,也无法正常运行

因此C++为了解决这种问题,提供模板的重载,可以为这些特定的类型提供具体化的模板. 利用具体化的模板,可以解决自定义类型的通用化.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
eg:
//具体化,显示具体化的原型和定义以template<>开头,并通过名称来指出类型
//具体化优先于常规模板
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;
}
}

类模板

语法:

1
2
template<typename T> // template<class T>

解释:

template — 声明创建模板

typename — 表面其后面的符号是一种数据类型,可以用class代替

T — 通用的数据类型,名称可以替换,通常为大写字母

类模板与函数模板区别

  1. 类模板没有自动类型推导的使用方式
  2. 类模板在模板参数列表中可以有默认参数

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

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

  • 普通类中的成员函数一开始(编译??)就可以创建
  • 类模板中的成员函数在调用时才创建(因为调用前不知道T是什么类

类模板对象做函数参数

一共有三种传入方式:

  1. 指定传入的类型 — 直接显示对象的数据类型
  2. 参数模板化 — 将对象中的参数变为模板进行传递
  3. 整个类模板化 — 将这个对象类型 模板化进行传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//1、指定传入的类型(最常用)
void printPerson1(Person<string, int> &p)
{
p.showPerson();
}
void test01()
{
Person <string, int >p("孙悟空", 100);
printPerson1(p);
}

//2、参数模板化
template <class T1, class T2>
void printPerson2(Person<T1, T2>&p)
{
p.showPerson();
//查看此时模板的类型
cout << "T1的类型为: " << typeid(T1).name() << endl;
cout << "T2的类型为: " << typeid(T2).name() << endl;
}
void test02()
{
Person <string, int >p("猪八戒", 90);
printPerson2(p);
}

//3、整个类模板化
template<class T>
void printPerson3(T & p)
{
cout << "T的类型为: " << typeid(T).name() << endl;
p.showPerson();

}
void test03()
{
Person <string, int >p("唐僧", 30);
printPerson3(p);
}

类模板与继承

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

  • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
  • 如果不指定,编译器无法给子类分配内存
  • 如果想灵活指定出父类中T的类型,子类也需变为类模板

类模板成员函数类外实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <string>

//类模板中成员函数类外实现
template<class T1, class T2>
class Person {
public:
//成员函数类内声明
Person(T1 name, T2 age);
void showPerson();

public:
T1 m_Name;
T2 m_Age;
};

//构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
this->m_Name = name;
this->m_Age = age;
}

//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
}

类模板分文件编写

问题:

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

解决:

  • 解决方式1:直接包含.cpp源文件
  • 解决方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制

类模板与友元

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

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

STL

函数对象

函数对象概念

概念:

  • 重载函数调用操作符的类,其对象常称为函数对象
  • 函数对象使用重载的()时,行为类似函数调用,也叫仿函数

本质:

函数对象(仿函数)是一个,不是一个函数

特点:

  • 函数对象在使用时,可以像普通函数那样调用, 可以有参数,可以有返回值
  • 函数对象超出普通函数的概念,函数对象可以有自己的状态
  • 函数对象可以作为参数传递

谓词

概念:

  • 返回bool类型的仿函数称为谓词
  • 如果operator()接受一个参数,那么叫做一元谓词
  • 如果operator()接受两个参数,那么叫做二元谓词

内建函数对象

内建函数对象意义

概念:

  • STL内建了一些函数对象

分类:

  • 算术仿函数

  • 关系仿函数

  • 逻辑仿函数

用法:

  • 这些仿函数所产生的对象,用法和一般函数完全相同
  • 使用内建函数对象,需要引入头文件 #include<functional>

算术仿函数

功能描述:

  • 实现四则运算
  • 其中negate是一元运算,其他都是二元运算

仿函数原型:

  • template<class T> T plus<T> //加法仿函数
  • template<class T> T minus<T> //减法仿函数
  • template<class T> T multiplies<T> //乘法仿函数
  • template<class T> T divides<T> //除法仿函数
  • template<class T> T modulus<T> //取模仿函数
  • template<class T> T negate<T> //取反仿函数

关系仿函数

功能描述:

  • 实现关系对比

仿函数原型:

  • template<class T> bool equal_to<T> //等于
  • template<class T> bool not_equal_to<T> //不等于
  • template<class T> bool greater<T> //大于
  • template<class T> bool greater_equal<T> //大于等于
  • template<class T> bool less<T> //小于
  • template<class T> bool less_equal<T> //小于等于

逻辑仿函数

功能描述:

  • 实现逻辑运算

函数原型:

  • template<class T> bool logical_and<T> //逻辑与
  • template<class T> bool logical_or<T> //逻辑或
  • template<class T> bool logical_not<T> //逻辑非

容器

STL基本概念

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

  • STL(Standard Template Library,标准模板库)
  • STL 从广义上分为: 容器(container) 算法(algorithm) 迭代器(iterator)
  • 容器算法之间通过迭代器进行无缝连接。
  • STL 几乎所有的代码都采用了模板类或者模板函数

STL六大组件

STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器

  1. 容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
  2. 算法:各种常用的算法,如sort、find、copy、for_each等
  3. 迭代器:扮演了容器与算法之间的胶合剂。
  4. 仿函数:行为类似函数,可作为算法的某种策略。
  5. 适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
  6. 空间配置器:负责空间的配置与管理。

STL中容器、算法、迭代器

容器:置物之所也

STL容器就是将运用最广泛的一些数据结构实现出来

常用的数据结构:数组, 链表,树, 栈, 队列, 集合, 映射表 等

这些容器分为序列式容器关联式容器两种:

序列式容器:强调值的排序,序列式容器中的每个元素均有固定的位置。
关联式容器:二叉树结构,各元素之间没有严格的物理上的顺序关系

算法:问题之解法也

有限的步骤,解决逻辑或数学上的问题,这一门学科我们叫做算法(Algorithms)

算法分为:质变算法非质变算法

质变算法:是指运算过程中会更改区间内的元素的内容。例如拷贝,替换,删除等等

非质变算法:是指运算过程中不会更改区间内的元素内容,例如查找、计数、遍历、寻找极值等等

迭代器:容器和算法之间粘合剂

提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。

每个容器都有自己专属的迭代器

迭代器使用非常类似于指针,初学阶段我们可以先理解迭代器为指针

迭代器种类:

种类 功能 支持运算
输入迭代器 对数据的只读访问 只读,支持++、==、!=
输出迭代器 对数据的只写访问 只写,支持++
前向迭代器 读写操作,并能向前推进迭代器 读写,支持++、==、!=
双向迭代器 读写操作,并能向前和向后操作 读写,支持++、–,
随机访问迭代器 读写操作,可以以跳跃的方式访问任意数据,功能最强的迭代器 读写,支持++、–、[n]、-n、<、<=、>、>=

常用的容器中迭代器种类为双向迭代器,和随机访问迭代器

image-20210407161123403

手册

cppreference[C++参考手册(已更新到20)]:https://en.cppreference.com/w/

LearnCpp[c++入门]:https://www.learncpp.com/

Cplusplus[论坛]:http://www.cplusplus.com/

TutorialsPoint[教程]:https://www.tutorialspoint.com/cplusplus/index.htm

Awesome C++[资源列表]: https://github.com/fffaraz/awesome-cpp

---------------- 本文结束 ----------------

本文标题:打怪之旅-C++基础

文章作者:Pabebe

发布时间:2021年03月01日 - 17:53:37

最后更新:2021年04月23日 - 21:48:50

原始链接:https://pabebezz.github.io/article/23d2b7d5/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%