C++基础入门

C++ 初识

Hello World

#include<iostream>
using namespace std;

int main() {
	cout << "Hello world" << endl;
	system("pause");
	return 0;
}

注释

在代码中加一些说明和解释,方便程序员阅读。编译器在编译代码时,会忽略注释的内容。

  • // 描述信息 :单行注释,放在一行代码的上方或者一条语句的末尾,对该行代码说明
  • /* 描述信息 */:多行注释,放在一段代码的上方,对该段代码做整体说明

变量

给一段指定内存空间起名,方便操作这段内存。创建变量时,最好初始化。

数据类型 变量名 = 初始值

常量

记录程序中不可更改的数据。创建常量时,必须初始化

  • #define 常量名 常量值:define宏常量,通常在文件最开始定义,由预处理器替换为字面值
  • const 数据类型 常量名 = 常量值:const修饰的变量

预保留标识符

asmdoifreturntypedef
autodoubleinlineshorttypeid
booldynamic_castintsignedtypename
breakelselongsizeofunion
caseenummutablestaticunsigned
catchexplicitnamespacestatic_castusing
charexportnewstructvirtual
classexternoperatorswitchvoid
constfalseprivatetemplatevolatile
const_castfloatprotectedthiswchar_t
continueforpublicthrowwhile
defaultfriendregistertrue
deletegotoreinterpret_casttry

自定义标识符

自定义标识符(对象名)命名的一些规则:

  • 标识符不能是关键字
  • 标识符只能由大小字母、数字、下划线组成
  • 第一个字符必须为字母或下划线,以方便语法解析
  • 见名知意

字面值

源代码中直接表示的固定值,编译器无需计算就能理解其值。

  • 十进制整数:42
  • 十进制无符号整数:42U
  • 十进制浮点数:3.14
  • 科学计数法:1.6e-19
  • 八进制整数:042
  • 十六进制整数:0x2a
  • 二进制整数:0b101010
  • 字符:'A'
  • 转义字符:'\n'
  • 字符串:"Hello"
  • 布尔:true
  • 指针:nullptrNULL

数据类型

创建一个对象时,必须要指定出相应的数据类型,表明该对象的大小,范围及其属性。

内置类型

整型占用取值范围示例
short2字节(-2^15 ~ 2^15-1)short num = 1
int4字节(-2^31 ~ 2^31-1)int num = 1
long4字节或8字节(-2^31 ~ 2^31-1)long num = 1
long long8字节(-2^63 ~ 2^63-1)long long num = 1
字符型
(signed) char1字节ASCII编码可显示字符集
ASCII编码不可显示转义字符集
char ch = 'a'
布尔型
bool1字节true(1) false(0)bool flag = true
浮点型
float4字节7位有效数字float f1 = 3.14
float f1 = 3.14f
float f2 = 3e2
double(双精度)8字节15位有效数字double d1 = 3.14

sizeof( 数据类型 / 对象)可以统计数据类型或对象所占字节数

自定义类型

string

通过面向对象特性或修饰属性(如数组、指针、引用、const或static等)自定义数据类型,以字符串为例:

  • string 字符串名 = "字符串值":C++风格字符串
  • char 字符串名[] = "字符串值":C风格字符串,需要一个字节存储结尾字符\0

结构体

通过结构体可以打包相关数据

  • struct 结构体名 { 结构体成员列表 };:定义结构体
  • 结构体名 结构体对象名 = { 成员1值 , 成员2值...}:创建结构体
  • 结构体对象名.成员:访问结构体成员
  • 结构体对象指针名->成员:访问结构体指针成员

指针

复合类型基本数据类型 *,用于保存基本数据类型对象的内存地址,长度由系统位数决定。

基本数据类型 *指针名 = &其他对象

  • 空指针NULL、nullptr、0:用于初始化指针,不可访问
  • 野指针:指针指向非法的内存空间,不可访问
  • &a:通过&取地址,得到对象内存地址
  • *p:通过*解引用,得到指针指向的值
  • const int *p1 = &a/int const *p1 = &a:指向常量的指针,指针指向可以改,指针指向的值不可以更改
  • int *const p2 = &a:指针常量,指针指向不可以改,指针指向的值可以更改
  • const int *const p1 = &a:指向常量的常量指针,指针指向不可以改,指针指向的值不可以更改

引用

复合类型基本数据类型 &,给已存在对象另外分配一个对象名 ,不是新对象,不占用独立内存空间。必须初始化为非临时对象且不可更改!

数据类型 &别名 = 原名

  • 不要返回局部变量/临时对象的引用,因为局部变量/临时对象可能会被销毁;
  • 函数返回引用可以作为左值,从而修改引用函数内部处理的对象

const int &p1 = &a/int const &p1 = &a:常量引用,引用常量值,可以初始化为临时对象或字面值

类型转换

不同内置数据类型之间可以进行强制类型转换

转换后数据类型 转换后对象 = (转换后数据类型)转换前对象

不同内置数据类型之间发生运算,会进行自动类型转换,如int和``unsgined之间运算时,int -> unsigned`会自动发生;同时,运算结果会自动进行类型推理。

数组

连续存放相同类型数据元素的集合

一维数组定义:

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

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

数据类型 数组名[数组长度]

二维数组定义:

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

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

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

数据类型 数组名[ 行数 ][ 列数 ]

  • 数组名[下标]...[下标]:数组访问

  • 数组可以理解为 数据类型 []数组类型

    • sizeof(arr):统计整个数组在内存中的长度;
    • &arr:数组首地址,与&arr[0] 地址值相同,但类型不同,意义不同
  • 大多表达式中会退化为数据类型 * const指针常量类型

    • arr + 1:下一个元素地址
    • *arr:首元素值
  • 函数参数中的数组名,实际是普通指针,除非使用引用int (&arr)[]

  • 多维数组的定义是一种嵌套,即二维数组是一维数组的数组

运算符

算数运算符术语示例结果
+正号+33
-负号-3-3
+10 + 515
-10 - 55
*10 * 550
/10 / 52
%整数取模10 % 31
++前置递增a=2; b=++aa=3; b=3
++后置递增a=2; b=a++a=3; b=2
前置递减a=2; b=–aa=1; b=1
后置递减a=2; b=a–a=1; b=2
赋值运算符
=赋值a=2; b=3a=2; b=3
+=加等于a=0; a+=2a=2
-=减等于a=5; a-=3a=2
*=乘等于a=2; a*=2a=4
/=除等于a=4; a/=2a=2
%=模等于a=3; a%=2a=1
比较运算符
==相等于4 == 30
!=不等于4 != 31
<小于4 < 30
>大于4 > 31
<=小于等于4 <= 30
>=大于等于4 >= 11
逻辑运算符
!!a真假反转
&&a && b有假为假
||a || b有真为真
IO运算符
«输出cout « acout
»输入cin » acin

不同运算符之间存在优先级,如输出运算符<<优先级大于比较运算符和逻辑运算符

程序流程结构

下面的结构间可以互相嵌套

if语句

if (condition) {
    /* code */
} 
else if (condition) {
    /* code */
}
// ...
else {
    /* code */
}

三目运算符

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

switch语句

switch (expression) {
    case constant expression:
        /* code */
        break;
	// ...
    default:
        break;
}
// expression为整型或者字符型
// case没有break,则会一直向下执行

while语句

while (condition) {
    /* code */
}

do…while语句

do
{
    /* code */
} while (condition);

for语句

for (size_t i = 0; i < count; i++) {
    /* code */
}

跳转语句

  • break:跳出最近的内层循环语句
  • continue:直接开始执行最内侧循环的下一轮
  • goto 标记:无条件跳转到标记:处开始执行

函数

定义

面向过程编程,将一段经常使用的代码封装起来,减少重复代码

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

调用

使用定义的封装代码

返回值类型 返回对象 = 函数名(实参)

声明

告诉编译器函数名称及如何调用函数,函数的实体另外定义。函数的声明可以多次,但是函数的定义只能有一次

返回值类型 函数名(参数类型 形参的列表)

参数传递

  • 值传递:形参和实参是不同地址空间的对象。如果形参发生变化,并不会影响实参。
  • 地址传递:以地址作为参数,形参和实参操作的是同一地址空间。形参可以修改实参的值。
  • 引用传递:形参是实参的别名,形参和实参操作的是同一对象。形参可以修改实参的值。

参数类型

  • (数据类型 参数= 默认值):默认参数,放在参数列表最后,调用时可省略默认,可被修改
    • 默认参数的默认值只能在函数定义或声明中指定一次,不能重复指定
  • (数据类型):占位参数,调用函数时必须填补该位置,可以有默认值

重载

相同的函数名作用于不同的参数类型,提高复用性,可重载条件如下:

  • 同一个作用域下
  • 函数名称相同
  • 函数参数不同,如个数、类型、顺序、引用与常量引用
    • 函数参数中存在默认参数时容易存在歧义重载

内存分区模型

  • 代码区:存放函数体的二进制机器指令,只读共享,由操作系统进行管理的
  • 全局区:存放全局变量和静态变量以及常量区存放常量和const修饰全局变量,由操作系统进行管理的
  • 栈区:程序运行后,由编译器自动分配释放, 存放函数的参数值、局部变量等,==这也是为什么不要返回局部变量的地址或者引用==
  • 堆区:程序运行后,由程序员通过new分配和delete释放,若程序员不释放,程序结束时由操作系统回收
    • 数据类型 *变量名 = new 数据类型(初始值):开辟数据,返回该数据对应的类型的指针
    • 数据类型 *数组名 = new 数据类型[初始大小]:开辟数组
    • delete 变量名:释放数据
    • delete[] 数组名:释放数组

分文件编写

  1. 创建后缀名为.h的头文件 ,引入必要库,说明源文件的声明

  2. 创建后缀名为.cpp的同名源文件,引入头文件,定义头文件声明

  3. 创建后缀名为.cpp的调用源文件,引入头文件,调用头文件声明

    模板不是真正的代码,而是代码的"模具"。编译器在实例化(调用)模板时,才根据具体类型生成真正的代码。

    分文件编写的类模板由于文件隔离,无法编译类实现

    因此,要求声明和实现在同一文件中,即.hpp

文件操作

#include <fstream>:ofstream写、ifstream读、fstream读写

| 文件打开方式,可配合|使用 | 解释 | | ——————— | ————————— | | ios::in | 为读文件而打开文件 | | ios::out | 为写文件而打开文件 | | ios::ate | 初始位置:文件尾 | | ios::app | 追加方式写文件 | | ios::trunc | 如果文件存在先删除,再创建 | | ios::binary | 二进制方式,默认以ASCII打开 |

// ASCII 文件
#include <fstream>
using namespace std;

int main() {
	ofstream ofs;
	ofs.open("test.txt", ios::out);
	ofs << "姓名:张三" << endl;
	ofs << "性别:男" << endl;
	ofs << "年龄:18" << endl;
	ofs.close();

    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;
	}
    ifs.close();
    
	return 0;
}
// binary 文件
#include <fstream>
using namespace std;

int main() {
	ofstream ofs;
	ofs.open("person.txt", ios::out | ios::binary);
	int p = 1;
	ofs.write((const char *)&p, sizeof(p));
	ofs.close();
	
	ifstream ifs;
    ifs.open("person.txt", ios::in | ios::binary);
	if (!ifs.is_open()) {
		cout << "文件打开失败" << endl;
	}
    int p;
	ifs.read((char *)&p, sizeof(p));
	ifs.close();

	return 0;
}

C++类和对象

封装

将属性和行为作为一个整体,加以权限控制

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

访问权限:

  • public:公共权限,类内可以访问,类外可以访问
  • protected:保护权限,类内可以访问,类外不可以访问
  • private:私有权限,默认访问权限【struct 默认访问权限为公共权限】,类内可以访问,类外不可以访问

访问对象:对象名.成员;|对象名->成员;

#pragma once
#include <iostream>
#include <cmath>
using namespace std;

class Point
{
    // friend ...
public:
	Point();		// 默认构造,即无参构造
	Point(double x, double y);		// 有参构造
    Point(const Point& point);		// 拷贝构造
	~Point() = default;		
	double getX();
	double getY();
	void setX(double x);
	void setY(double y);
    
    static void setFlag(int target);
    void constfix() const;
    void nullCheck();
public:
    mutable int index;
    static int flag;
private:
	double x;
	double y;
};

class Circle
{
public:
	Circle() = default;
    // 默认构造,对应成员属性的默认初始化
	Circle(double radius, Point center);
	~Circle() = default;
	double getRadius();
	Point getCenter();
	void setRadius(double radius);
	void setCenter(Point center);
private:
	double radius;
	Point center;
};
#include "circle.h"

Point::Point(): x(0), y(0) {}
Point::Point(double a, double b): x(a), y(b) {}
Point::Point(const Point& point) {
    this->x = point.x;
    this->y = point.y;
}
double Point::getX() {
    return x;
}
double Point::getY() {
    return y;
}
void Point::setX(double x) {
    this->x = x;
}
void Point::setY(double y) {
    this->y = y;
}
void Point::setFlag(int target) {
    flag = target;
}
int Point::flag = 0;
void Point::constfix() const {
    this->index = 1;
    cout<< this->index << endl;
}

void Point::nullCheck() {
    if(this == NULL) return;
    cout<< this->x << ',' << this->y << endl;
}

Circle::Circle(double radius, Point center) {
    this->radius = radius;
    this->center = center;
}
double Circle::getRadius() {
    return radius;
}
Point Circle::getCenter() {
    return center;
}
void Circle::setRadius(double radius) {
    this->radius = radius;
}
void Circle::setCenter(Point center) {
    this->center = center;
}
// 无参构造
Point p1;  
Point p2{};  // Point p11 = {};

// 有参构造
Point p3(10, 20);
Point p4{5, 15};  // Point p10 = {5, 15};
// 拷贝构造
Point p6(p3);         // 显示拷贝构造
Point p5 = p3;        // 隐式拷贝构造
Point p7 = Point(p3); // 匿名对象拷贝构造
// 拷贝赋值运算符
Point p8;
p8 = p3;
// 移动赋值运算符
Point p9;
p9 = Point(p3);
p9 = std::move(p2);
// 不允许单独构造匿名对象
// Point(p4);
// 这是一个函数声明
 // Point p1();

p1.flag = 1;  				// Point::flag
p1.setFlag(1);				// Point::setFlag(1);

Point *p = NULL;
p->nullCheck();

const Point pa;
pa.constfix();

封装模型

┌─────────────────────────────┐
│       代码区 (Text)          │
│  - 所有可执行代码		     │
│  - 成员函数代码(共享)		   │
│  - 虚函数表(vtable)	      │
├─────────────────────────────┤
│       数据区 (Data)          │
│  - 全局变量        		   │
│  - 静态成员变量(共享)		   │
│  - 字符串常量		   		  │
├─────────────────────────────┤
│       BSS区 (未初始化数据)    │ 
│  - 未初始化数据	    	      │
├─────────────────────────────┤
│       堆 (Heap)             │ ← new创建的对象
│  ┌─────────────────────┐    │
│  │ 对象1: 			    │    │
│  │  - 虚函数表指针(vptr) │    │
│  │  - int, char...     │    │
│  └─────────────────────┘    │
│  ┌─────────────────────┐    │
│  │ 对象2: ... 		    │    │
│  └─────────────────────┘    │
├─────────────────────────────┤
│           栈 (Stack)        │ ← 局部对象
│  ┌─────────────────────┐    │
│  │ 对象3: 			    │    │
│  │  - 虚函数表指针(vptr) │    │
│  │  - int, char...     │    │
│  └─────────────────────┘    │
└─────────────────────────────┘
  • 空对象占用一个字节,以确保每个对象在内存中都有唯一的地址,以作区分。
  • 空指针对象可以调用成员函数,但每个非静态成员函数调用都隐式地传递了一个 this 指针参数,若函数体内使用空指针对象的this时,会发生错误,否则无碍。

构造函数

对象自动初始化,可以重载,编译器提供空实现的无参构造、浅拷贝构造和赋值重载

类名(形参列表){}

  • 如果定义了有参构造函数,编译器不再提供无参构造;如果定义了拷贝构造函数,不再提供其他构造函数
  • 当成员属性中存在指针类型,且没有自定义深拷贝构造和赋值【在堆区重新申请空间拷贝成员指针变量指向的值】,存在重复地址释放的风险

析构函数

对象自动销毁清理,不可重载,编译器提供空实现的无参析构

~类名(){

  • 对象按执行顺序构造,析构反之

  • 构造嵌套类时,0先构造对象成员,再构造本对象,析构反之

  • 构造子类时,先构造父类,再构造子类,析构反之

  • 当属性中存在堆区数据时,最好在析构函数中手动销毁

    if(p!=nullptr) {
    	delete p;
    	p = nullptr;
    }
    

修饰

this指针

类预定义指针常量,隐含在每个非静态成员中,指向被调用成员所属的对象

  • 当形参和成员变量同名时,可用 this 指针来区分
  • 在类的非静态成员函数中返回对象本身,即 return *this

static修饰成员

在成员变量和成员函数前加上关键字static,静态成员供类下所有对象共享

  • 静态成员变量要求类内声明,类外初始化
  • 静态成员函数只能访问静态成员变量

const修饰成员

在成员函数参数列表后加上关键字const,const成员不允许对成员变量进行修改

  • 常函数不可以修改成员变量,但成员变量声明时加关键字mutable后,在常函数中依然可以修改;
  • 常对象只能调用常函数或修改mutable变量

explicit修饰构造

用于修饰单参数构造函数或转换构造函数,阻止编译器进行不希望的隐式类型转换,让代码更安全、更清晰

友元

有些私有属性让类外一些特殊的函数或者类进行访问

全局函数

friend void visit(Building * building);

friend class goodGay;

成员函数

friend void goodGay::visit(Building * building);

运算符重载

重定义运算符像定义函数一样,可以是全局函数,也可以是成员函数。

class Person
{
public:
	Person() = default;
	Person(string name) {
		this->name = name;
        this->total = 1;
	};
	~Person() = default;

public:
	string name;
    int total;

public:
    // 成员函数实现 + 重载
    // p_object + p_param 或者 p_object.operator+(p_param)
    Person operator+(const Person& p) {
        Person temp("sum");
        temp.total = this->total + p.total;
        return temp;
    }

    // 成员函数实现 << 重载
    // p_object << cout 或者 p_object.operator<<(cout) 左附加
     ostream& operator<<(ostream& os) {
        os << this->name << " " << this->total << endl;
        return os;
    }

    // 成员函数实现 前置++ 重载
    // ++p_object 或者 p_object.operator++()
    Person& operator++() {
        this->total++;
        return *this;
    }

    // 成员函数实现 后置++ 重载
    // p_object++ 或者 p_object.operator++(0)
    Person operator++(int) {
        Person temp = *this;
        this->total++;
        return temp;
    }

    // 成员函数实现 = 重载
    // p_object = p_param 或者 p_object.operator=(p_param) 右优先
    Person& operator=(const Person& p) {
        this->name = p.name;
        this->total = p.total;
        return *this;
    }

    // 成员函数实现 == 重载
    // p_object == p_param 或者 p_object.operator==(p_param)
    bool operator==(const Person& p) {
        return this->total == p.total;
    }

    // 成员函数实现 () 重载
    // p_object(string_param) 或者 p_object.operator()(string_param) 仿函数
    // 与匿名对象Person(string_param)有点像
    void operator()(string name) {
        this->name = name;
        return;
    }
}; 
//全局函数实现 + 重载
//p1 + p2 或者 operator+(p1, p2)
Person operator+(const Person& p1, const Person& p2) {
    Person temp("sum");
    temp.total = p1.total + p2.total;
    return temp;
}

//全局函数实现 << 重载
//cout << p 或者 operator<<(cout, p) 右附加
ostream& operator<<(ostream& os, const Person& p) {
    os << p.name << " " << p.total << endl;
    return os;
}

继承

呈树型的类关系,派生类(子类)除了拥有基类(父类)的成员,还有自己独有的成员。

继承类:class 子类: 继承方式 父类{};,可多继承,即 class 子类: 继承方式1 父类1, 继承方式2 父类2...{};

继承方式:

  • public公共继承:保持成员访问权限,无法访问父类的私有成员
  • protected保护继承:成员访问权限保护化,无法访问父类的私有成员
  • private私有继承:成员访问权限私有化,无法访问父类的私有成员
class Animal
{
public:
    Animal() = default;
    Animal(string name) : name(name) {}
    virtual ~Animal() = default;

    virtual void makeSound() const
    {
        cout << name << " makes a sound." << endl;
    };

    string getName() const
    {
        return name;
    }
    string name;
};

class Dog : virtual public Animal
{
public:
    Dog(string name) : Animal(name) {}
    ~Dog() = default;

    void makeSound() const override
    {
        cout << name << " says: Woof!" << endl;
    }

    int DogId;
};

class Cat : virtual public Animal
{
public:
    Cat(string name) : Animal(name) {}
    ~Cat() = default;

    void makeSound() const override
    {
        cout << name << " says: Meow!" << endl;
    }

    int CatId;
};

class DogCat : public Dog, public Cat
{
public:
    DogCat(string name) : Animal(name), Dog(name), Cat(name) {}
    ~DogCat() = default;

    void makeSound() const override
    {
        Dog::makeSound();
        Cat::makeSound();
    }
};

继承模型

DogCat 菱形继承对象:
+-----------------------+  ← 0x00 (Dog部分开始)
| Dog::Animal::vptr (8) | → Dog::Animal 虚函数表(指向DogCat覆盖后的版本)
+-----------------------+  ← 0x08
| Dog::Animal::name (32)| → Animal::name (Dog版本)
+-----------------------+  ← 0x28
| Dog::Animal::DogId (4)|
| 对齐填充 (4)           |
+-----------------------+  ← 0x30 (Cat部分开始)
| Cat::Animal::vptr (8) | → Cat::Animal 虚函数表(指向DogCat覆盖后的版本)
+-----------------------+  ← 0x38
| Cat::Animal::name (32)| → Animal::name (Cat版本)
+-----------------------+  ← 0x58
| Cat::Animal::CatId (4)|
| 对齐填充 (4)           |
+-----------------------+  ← 0x60 (DogCat自身成员开始)
| DogCat成员(暂无)      |
+-----------------------+
总大小:~96字节+(两个完整的Animal副本)

DogCat 虚继承对象:
+-----------------+  ← 0x00 (Dog部分开始)
| Dog::vptr (8)   | → Dog虚函数表(指向DogCat覆盖后的版本,包含虚基类偏移信息)
+-----------------+  ← 0x08
| Dog::DogId (4)  |
| 对齐填充 (4)     |
+-----------------+  ← 0x10 (Cat部分开始)
| Cat::vptr (8)   | → Cat虚函数表(指向DogCat覆盖后的版本,包含虚基类偏移信息)
+-----------------+  ← 0x18
| Cat::CatId (4)  |
| 对齐填充 (4)     |
+-----------------+  ← 0x20 (DogCat自身成员开始)
| DogCat成员(暂无)|
+-----------------+  ← 0x28 (Animal虚基类部分开始)
| Animal::vptr (8)| → Animal虚函数表(指向DogCat覆盖后的版本)
+-----------------+  ← 0x30
| Animal::name(32)| → 唯一的name对象
+-----------------+
总大小:~72字节+
Dog虚函数表(在DogCat中):
+---------------------+
| typeinfo for DogCat |
+---------------------+
| offset to top: 0    |
+---------------------+
| vbase offset: 40    | ← 从Dog子对象到Animal的偏移
+---------------------+
| Dog::makeSound()    | ← 实际指向DogCat::makeSound()
+---------------------+

Cat虚函数表(在DogCat中):
+---------------------+
| typeinfo for DogCat |
+---------------------+
| offset to top: 16   | ← 到DogCat对象顶部的偏移
+---------------------+
| vbase offset: 24    | ← 从Cat子对象到Animal的偏移
+---------------------+
| Cat::makeSound()    | ← 实际指向DogCat::makeSound()
+---------------------+

Animal虚函数表(在DogCat中):
+---------------------+
| typeinfo for DogCat |
+---------------------+
| DogCat::makeSound() | ← 实际指向DogCat::makeSound()
+---------------------+
  • 子类对象访问同名成员,子类同名成员直接访问,父类同名成员使用作用域区分访问
  • 产生菱形继承时,孙类会继承了爷类两份重复成员,无法直接访问重复成员,但可以使用作用域区分访问。虚继承解决菱形继承重复数据的问题

多态

不同对象对同一函数做出不同的响应

  • 编译时静态多态:函数重载、运算符重载、模板
  • 运行时动态多态:通过虚函数和继承实现
    • 父类指针(或引用)可以指向子类对象;
    • 子类可以重载父类的虚函数:未重载时该函数采用父类实现,重载时该函数采用子类实现;
    • 父类指针指向子类对象时,可以调用父类中定义的所有函数, 可以调用子类重写的虚函数,不能直接调用子类特有的函数
void speak(Animal* a) {
    a->makeSound();
    delete a; 
}
speak(new Cat("Fluffy"));
speak(new Dog("Rover"));

多态模型

当一个类声明了至少一个虚函数(或虚继承),编译器就会为这个类生成一个唯一的虚函数表,表中的每个条目指向该类的一个虚函数的实际代码地址或或虚基类地址。

当你通过父类指针或引用调用虚函数时,通过下面的对象模型找到实际的虚函数表及其虚函数实现:

Dog对象内存布局(独立时):
+----------------+
| Dog::vptr (8)  | → Dog虚函数表
+----------------+
| DogId (4)      |
| padding (4)    | → 对齐到8字节边界
+----------------+
| Animal部分     | → 虚基类
| - Animal::vptr |
| - name         |
+----------------+

Cat对象内存布局(独立时):
+----------------+
| Cat::vptr (8)  | → Cat虚函数表
+----------------+
| CatId (4)      |
| padding (4)    |
+----------------+
| Animal部分     | → 虚基类
| - Animal::vptr |
| - name         |
+----------------+
Dog vtable for Dog (独立对象):
+---------------------+
| typeinfo for Dog    |  ← RTTI信息
+---------------------+
| offset to top: 0    |  ← 到Dog对象顶部的偏移(总是0)
+---------------------+
| vbase offset: 16    |  ← Dog子对象到Animal部分的偏移
                       // 计算公式:Animal地址(0x10) - Dog地址(0x00) = 0x10(16)
+---------------------+
| Dog::~Dog()         |  ← 析构函数(Dog版本)
+---------------------+
| Dog::makeSound()    |  ← Dog自己的makeSound实现
+---------------------+

Animal vtable for Dog (独立对象):
+---------------------+
| typeinfo for Dog    |  ← RTTI信息
+---------------------+
| Dog::~Dog()         |  ← 析构函数(最终覆盖)
+---------------------+
| Dog::makeSound()    |  ← Dog覆盖的makeSound
+---------------------+

Cat vtable for Cat (独立对象):
+---------------------+
| typeinfo for Cat    |  ← RTTI信息
+---------------------+
| offset to top: 0    |  ← 到Cat对象顶部的偏移(总是0)
+---------------------+
| vbase offset: 16    |  ← Cat子对象到Animal部分的偏移
                       // 计算公式:Animal地址(0x10) - Cat地址(0x00) = 0x10(16)
+---------------------+
| Cat::~Cat()         |  ← 析构函数(Cat版本)
+---------------------+
| Cat::makeSound()    |  ← Cat自己的makeSound实现
+---------------------+

Animal vtable for Cat (独立对象):
+---------------------+
| typeinfo for Cat    |  ← RTTI信息
+---------------------+
| Cat::~Cat()         |  ← 析构函数(最终覆盖)
+---------------------+
| Cat::makeSound()    |  ← Cat覆盖的makeSound
+---------------------+
  • 当基类无法提供有意义的默认实现,或者需要强制派生类必须实现某个功能时,可以使用纯虚函数。

    • 纯虚函数会使类成为抽象类,抽象类不能被实例化,其派生类必须重写所有的纯虚函数才能成为具体类。
    virtual void makeSound() const = 0;
    
  • 当基类的析构函数不是虚函数时,如果通过基类指针删除派生类对象,那么只会调用基类的析构函数,派生类的析构函数不会被调用。这可能导致派生类独有的资源(如动态分配的内存、文件句柄等)无法正确释放,从而产生资源泄漏。

    • 将基类的析构函数声明为虚函数后,通过基类指针删除派生类对象时,会首先调用派生类的析构函数,然后自动调用基类的析构函数。

    • 纯虚析构函数也可以使类成为抽象类,但与其他纯虚函数不同,纯虚析构函数必须在类外提供具体的实现(即使是空实现)。这是因为在销毁派生类对象时,仍然需要调用基类的析构函数来完成对象销毁链。

C++ 模板

编写与具体类型无关的代码,在编译时由编译器根据实际使用的类型自动生成具体版本(可以重载)

template <typename T>:告诉编译器下面这个函数或类里的 T 是一个类型占位符,且使用相同的T,可以定义多个同名的T供不同个对象使用

基本使用

函数模板

bool myCompare(int a, int b)
{
    cout << "普通函数被调用" << endl;
    return a != b;
}

template <typename T>
bool myCompare(T a, T b)
{
    cout << "泛化模板函数被调用" << endl;
    return a != b;
}

template <>
bool myCompare<int *>(int *a, int *b) {
    cout << "特化模板函数被调用" << endl;
    return *a != *b;
}

template bool myCompare<int>(int a, int b); // 声明显式实例化

// 隐式实例化
myCompare(1, 1); // 自动推导,int, 不可隐式转换
myCompare<int>(1, 1); // 显式指定, int,可隐式转换
// myCompare(3, 3.14)  error, 类型不一致或不确定
myCompare<>(1, 1);    // 强制模板, int

调用优先级:

  1. (显示指定模板|空模板)强制调用函数模板
  2. 精确匹配的普通函数
  3. 特化函数模板/显示实例化
  4. 泛化函数模板
  5. 需要隐式类型转换的普通函数

类模板

// 类外实现成员函数
template<typename T1, typename T2>
class Person {
public:
	Person(T1 name, T2 age);
public:
	T1 m_Name;
	T2 m_Age;
};

template<typename T1, typename T2>
Person<T1, T2>::Person(T1 name, T2 age) {
	this->m_Name = name;
	this->m_Age = age;
}

// 含默认参数
template<typename NameType, typename AgeType = int> 
class Person {
public:
    Person(NameType name, AgeType age) 
        : mName(name), mAge(age) {}
private:
    NameType mName;
    AgeType mAge;
};

// 部分特化的类模板成员不需要和泛化模板成员保持完全一致
template <typename NameType>
class Person<NameType, int> {
public:
    Person(NameType name, int age)
		: mName(name), mAge(age) {}
    void func() {
        cout << "这是模板的部分特化版本" << endl;
    }
private:
    NameType mName;
    int mAge;
};

// 只允许显式指定,不存在自动推导
Person<string> p1("孙悟空", 999); 
Person<string, double> p2("唐僧", 30.5);

特殊用法

// 非类型参数类模板,是编译期常量
template <typename NameType, int N>
class Person {
public:
    Person(NameType name, int age) 
        : mName(name), mAge(age) {}
private:
    NameType mName;
    int mAge;
    int mDefaultAge = N;
};

Person<string, 100> p("默认年龄100", 50);
// 嵌套模板
template <typename T template<typename> class Container>
class Wrapper {
public:
    Wrapper(const Container<T> &d) : data(d) {}
    void display() {
        for (const auto &item : data)
            cout << item << " ";
        cout << endl;
    }
private:
    Container<T> data;
};

vector<int> v = {1, 2, 3, 4, 5};
Wrapper<vector, int> example(v);
example.display();
// 模板参数传递
void printPerson1(Person<string> &p);  // 指定传入的类型

template <typename T>
void printPerson2(Person<T> &p);   // 模板参数模板化

template<typename T>
void printPerson3(T & p);			// 模板整体模板化
// 模板继承
class Son :public Base<int>
    
template<typename T>
class Son :public Base<T>		// 灵活指定
// 模板友元
// 友元全局函数类内实现
template<typename T1, typename T2>
class Person
{
	friend void printPerson(Person<T1, T2> & p) {
		cout << "姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
	}
    // ...
};
printPerson(p);

// 友元全局函数类外实现
template<typename T1, typename T2> class Person;
template<typename T1, typename T2>
void printPerson(Person<T1, T2> & p);

template<typename T1, typename T2>
class Person
{
	friend void printPerson<>(Person<T1, T2> & p);
    // ...
};

template<typename T1, typename T2>
void printPerson(Person<T1, T2> & p)
{
	cout << "类外实现 ---- 姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
}

printPerson(p);

举例

#pragma once
#include <iostream>
using namespace std;

template<class T>
class MyArray
{
public:
	MyArray() = default;
	MyArray(int capacity) {
		this->m_Size = 0;
		this->m_Capacity = capacity;
		pAddress = new T[this->m_Capacity];
	}
	MyArray(const MyArray & arr) {
		this->m_Size = arr.m_Size;
		this->m_Capacity = arr.m_Capacity;
		this->pAddress = new T[this->m_Capacity];
		for (int i = 0; i < this->m_Size; i++) {
			this->pAddress[i] = arr.pAddress[i];
		}
	}
    ~MyArray() {
		if (this->pAddress != NULL) {
			delete[] this->pAddress;
			this->pAddress = NULL;
			this->m_Capacity = 0;
			this->m_Size = 0;
		}
	}

	MyArray& operator=(const MyArray& myarray) {
		if (this->pAddress != NULL) {
			delete[] this->pAddress;
			this->pAddress = NULL;
			this->m_Capacity = 0;
			this->m_Size = 0;
		}
		this->m_Size = myarray.m_Size;
		this->m_Capacity = myarray.m_Capacity;
		this->pAddress = new T[this->m_Capacity];
		for (int i = 0; i < this->m_Size; i++) {
			this->pAddress[i] = myarray[i];
		}
		return *this;
	}

	T& operator[](int index) {
		return this->pAddress[index]; //不考虑越界,用户自己去处理
	}

	void Push_back(const T & val) {
		if (this->m_Capacity == this->m_Size) {
			return;
		}
		this->pAddress[this->m_Size] = val;
		this->m_Size++;
	}

	void Pop_back() {
		if (this->m_Size == 0) {
			return;
		}
		this->m_Size--;
	}

	int getCapacity() {
		return this->m_Capacity;
	}

	int	getSize() {
		return this->m_Size;
	}

private:
	T * pAddress;  
	int m_Capacity;
	int m_Size;
};

C++ STL

标准数据结构和算法,容器算法之间通过迭代器进行无缝连接,还有仿函数适配器空间配置器

容器

类型:string、vector、deque、stack、queue、list、set、pair、map

方法:构造、赋值、存取、插入、删除、修改、属性

算法

遍历、比较、查找、排序、拷贝、替换、交换、算数、集合

函数对象

重载函数调用操作符的类,类似于函数一样使用,有状态,可作为参数传递

class MyAdd
{
public :
    MyAdd() {
        this->count=0;
    }
	int operator()(int v1,int v2) {
        this->count++;
		return v1 + v2;
	}
    int count;
};
MyAdd myAdd;
myAdd(10, 10);
// myAdd.count

加法plus、减法minus、乘法multiplies、除法divides、取模modulus、取反negate

等于equal_to、不等于not_equal_to、大于greater、大于等于greater_equal、小于less、小于等于less_equal

与logical_and、或logical_or、非logical_not