前序知识
变量:给一段指定的内存空间起名,方便操作这段内存。
C++定义常量的两种方式:
- #define宏常量:
#define 常量名 常量值
- const修饰的变量:
const 数据类型 常量名 = 常量值
标识符的命名规则:
- 标识符不能是关键字
- 标识符只能由字母、数字、下划线组成
- 第一个字符必须为字母或下划线
- 标识符字母区分大小写
1 初识知识
1.1数据类型
数据类型存在的意义:给变量分配合适的内存空间
1.1.1整型
image-20240229211305122
1.1.2浮点型
image-20240229211916435
1.1.3字符型
字符型变量用于显示单个字符
语法:char ch = 'a'
注意:
- C和C++中字符型变量只占用1个字节
- 字符型变量并不是把字符本身放到内存中进行存储,而是将对应的ASCII码存储
1.1.4转义字符
用于表示一些不能显示出来的ASCII字符
image-20240229212521459
1.1.5字符串
C风格字符串:char str[ ] = "abcdefg"
C++风格字符串:string str = "abcdefg"
1 2 3 4 5 6 7 8 9 10
| #include <iostream> #include <string>
using namespace std; int main() { char str1[]="abcd"; string str2="abcd"; return 0; }
|
1.1.6布尔类型
作用:布尔数据类型代表真或者假的值
bool占用1个字节的大小
1.1.7数据的输入和输出
关键字:cin、cout
语法:cin >> 变量名和cout << 变量名
1.1.8程序的流程结构
C/C++支持最基本的三种程序运行结构:==顺序结构==、==选择结构==、==循环结构==
1.1.9一维数组
一维素组定义的三种方式:
int a[10]={1,2,3,4}
int a[]={1,2,3,4}
1.1.10一维数组-冒泡排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <iostream> using namespace std; int main() { int a[] = {4,5,3,10,2,8,32}; for (int i = 0; i < 7; i++) { for (int j = 0; j < 7-i-1; j++) { if (a[j] < a[j + 1]) { int temp = a[j]; a[j] = a[j + 1]; a[j + 1] = temp; } } } for (int i = 0; i < 7; i++)cout << a[i] << '\t'; return 0; }
|
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,执行完毕后,找到第一个最大值。
- 重复上面的步骤,每次比较数-1,直到不需要比较。
img
1.1.11二维数组
定义方式:
int a[2][2]={{数据1,数据2},{数据3,数据4}}
int a[2][2]={数据1,数据2,数据3,数据4}
int a[][2]={数据1,数据2,数据3,数据4}
注意:必须要指明列数,行数可以无需指明
*1.1.12函数的分文件编写
步骤:
- 创建后缀名为.h的头文件,在头文件中写函数声明
- 创建后缀名为.cpp的源文件,在源文件中写函数的实现
main文件
1 2 3 4 5 6 7 8 9 10
| #include <iostream> #include "swap.h" using namespace std; int main() { int a=0,b=1; swap(&a,&b); cout<<a<<b<<endl; return 0; }
|
swap.h
1
| void swap(int *a,int *b);
|
swap.cpp
1 2 3 4 5 6 7
| #include "swap.h" void swap(int *a,int *b) { int temp=*a; *a=*b; *b=temp; }
|
1.1.13指针相关
指针的本质就是一个十六进制的数,用于表示地址,在32位下占用4字节,64位下占用8字节
空指针:指向编号为0的空间,用于初始化指针变量,int *a = NULL,
注意:空指针是==不可访问的==,0~255之间的编号是系统占用的,因此不可访问
野指针:指向非法的内存空间(不是自己申请的空间),int *p=(int *)0x1100
const修饰指针:
- const修饰指针:常量指针
const int * p
- const修饰常量:指针常量
int * const p
- const既修饰指针,又修饰常量
const int * const p
1 2 3 4
| const int *p = &a; p = &b; *p = 20;
|
1 2 3 4
| int * const p = &a; p = &b; *p = 20;
|
看const位置,只要const紧跟的就可以修改,可以理解为锁定变量指针
1.1.14结构体
语法:struct 结构体名 {成员列表}
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| struct student { string name; int age; int score; };
int main() { struct student Stu1={"ZhangSan",18,90}; student Stu2 = {"LiSi",18,80}; return 0; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| struct student { string name; int age; int score; }Stu;
int main() { Stu.name="ZhangSan"; Stu.age=18; Stu.score=90; return 0; }
|
1.2 climits头文件
定义了各种整型数据的最大值和最小值
image-20240305105400635
image-20240305105423398
1
| #define INT_MAX 2147483647
|
头文件中有许多如上的指令
#define也是一个预处理编译指令,告诉编译器将INT_MAX替换为2147483647
2 C++核心编程
2.1内存分区模型
C++程序在执行时,将内存大方向划分为4个区域
- 代码区:用于存放函数的二进制代码,由操作系统进行管理
- 全局区:存放全局变量和静态变量(static)以及常量(const)
- 栈区:由编译器自动分配释放,存放函数参数和局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束由操作系统回收
内存分区的意义:不同区域存放的数据,赋予不同的生命周期,更为灵活的编程
代码区和全局区都是在程序运行前就存在,栈区和堆区都在程序运行时才会分配
2.1.1堆区开辟和释放
1 2 3 4
| int * p1 = new int(10); int * p2 = new int[20]; int * p3 = (int *)malloc(20*sizeof(int));
|
2.2引用&
本质就是指针常量(int* const ref = &a),指向不可改变,值可改
注意,b并不是指针,是对a的引用,给一个变量起别名
区别就在于,引用必须初始化,创建后不可以改变指向,指针可以修改指向
注意:引用可以用作函数返回值,但是不要返回局部变量的引用,因为在函数结束后,栈区会释放。
2.3函数提高
2.3.1函数形参
在C++中,函数的形参列表可以设置默认值
1 2 3 4 5
| int func(int a,int b = 10, int c=20) { }
|
如果某个位置已经有了默认参数,那么从这个位置往后,都必须有默认参数
1
| int func(int a,int b =10, int c)
|
函数的声明和实现只能一个有默认参数,不能同时有
1 2 3 4 5
| int func(int a,int b=10,int c=20); int func(int a, int b,int c){ }
int func(int a, int b,int c); int func(int a, int b=10, int c=20){ }
|
2.3.2占位参数
1
| void func(int a, int){ }
|
调用函数时必须填补该参数
2.3.3函数重载
作用:函数名可以相同,提高复用性
- 同一个作用域下,函数名相同
- 函数参数 类型不同 或 个数不同 或
顺序不同
1 2 3 4 5 6 7 8
| void func(int a) { cout<<"int函数:"<<a<<endl; } void func(double a) { cout<<"double函数:"<<a<<endl; }
|
注意:
函数重载碰到默认参数时,默认参数是不计的
1 2 3
| int func(int a,int b=10,int c=20){ } int func(int a);
|
函数重载碰到引用时,
1 2 3
| void func(int &a)
void func(const int &a)
|
2.4类和对象
C++面向对象的三大特征:封装、继承、多态
万事万物皆为对象,对象上有其属性和行为
- struct 默认权限为公共 public
- class 默认权限为私有 private
2.4.1封装
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <iostream> #include <string> using namespace std; class circle { public: double r; const double PI = 3.1415926; double calculateZC() { return 2 * PI * r; } }; int main() { circle c1; c1.r = 1.0; cout << c1.calculateZC() << endl; return 0; }
|
2.4.2权限
- public 公共权限
- protected 保护权限
- private 私有权限(默认)
1 2 3 4 5 6 7 8 9 10 11 12
|
class Person { public: string name; protected: string id; private: int password; };
|
类中成员的默认权限为private
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
| #include <iostream> #include <string> using namespace std; class Cube { private: double l, w, h; public: void setLWH(double ll, double ww, double hh) { l = ll; w = ww; h = hh; } double getV() { return l * w * h; } double getA() { return 2 * (l * w + w * h + l * h); } bool ifequ(Cube x) { return x.l == l && x.h == h && x.w == w ? true : false; } }; int main() { Cube a,b; a.setLWH(1, 2, 3); b.setLWH(1, 2, 3); cout << "面积为:" << a.getA() << "体积为:" << a.getV() << endl; cout << "是否相等" << a.ifequ(b) << endl; return 0; }
|
2.4.3对象的初始化和清理(构造和析构)
如果我们不提供构造函数和析构函数,编译器会提供空实现的构造函数和析构函数
- 构造函数:主要作用在于创建对象时为对象成员属性赋值,构造函数由编译器自动调用,无需手动调用
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作
构造函数可以有参数,可以发生重载
析构函数不能有参数,不能发生重载
1 2 3 4 5 6 7 8 9 10 11 12
| class Person { public: Person() { } ~Person() { } };
|
2.4.3.1构造函数的分类及调用
分类:有参构造、无参构造(普通构造)/拷贝构造
调用:括号法、显示法、隐式转换法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Person { public: int age; Person() { cout<<"无参构造函数调用"<<endl; } Person(int a) { age = a; cout<<"有参构造函数调用"<<endl; } Person(const Person &p ) { age = p.age; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void test() { 1、括号法 Person p1; Person p2(10); Person p3(p2);
2、显示法 Person p2 = Person(10); Person p3 = Person(p2); 补充:Person(10); 3、隐式转换法 Person p4 = 10; Person p5 = p4; }
|
2.4.3.2拷贝构造函数调用时机
- 使用一个已经创建完毕的对象来初始化新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
1 2 3 4 5
| void test01() { Person p; Person p1(p); }
|
1 2 3 4 5 6 7 8
| void doWork(Person p) { } void test02() { Person p; doWork(p); }
|
1 2 3 4 5 6 7 8 9
| Person doWork() { Person p3; return p3; } void test03() { Person p = doWork(); }
|
2.4.3.3构造函数的调用规则
默认的拷贝构造函数会进行值拷贝,其他的构造函数都是空实现的
- 如果用户定义了有参构造函数,C++不再提供默认的无参构造函数,但会提供默认的拷贝构造
- 如果用户定义了拷贝构造函数,C++不会再提供其他构造函数
2.4.3.4深拷贝和浅拷贝
浅拷贝:简单的赋值拷贝操作(=)
深拷贝:在堆区重新申请内存空间,进行拷贝操作
拷贝构造带来的问题:
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
| #include <iostream> #include <string> using namespace std; class Stu { public: string name; int* m_height; Stu(string a, int height) { name = a; m_height = new int(height); } ~Stu() { if (m_height != NULL) { delete(m_height); m_height = NULL; } } }; int main() { Stu c1("vicczyq", 172); Stu c2(c1); return 0; }
|
这段代码,c2会拷贝c1的值,但是他拷贝的是指针,是同一个地址,当c2先析构后,c1析构时候释放内存会出错崩溃
image-20240301224532559
因此,在进行拷贝时,要新申请一个空间,这样才能解决
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
| #include <iostream> #include <string> using namespace std; class Stu { public: string name; int* m_height; Stu(string a, int height) { name = a; m_height = new int(height); } Stu(const Stu &p) { m_height = new int(*p.m_height); name = p.name; } ~Stu() { if (m_height != NULL) { delete(m_height); m_height = NULL; } } }; int main() { Stu c1("vicczyq", 172); Stu c2(c1); return 0; }
|
2.4.3.5初始化列表
1 2 3 4 5 6 7 8 9
| class Person { public: int m_a,m_b,m_c; Person(int a, int b, int c) : m_a(a),m_b(b),m_c(c) { } };
|
2.4.3.6类对象作为类成员
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
| #include <iostream> #include <string> using namespace std; class A { public: A() { cout << "A构造" << endl; } }; class B { A a; public: B() { cout << "B构造" << endl; } }; int main() { B x; return 0; }
|
image-20240301230820745
证明,构造B时,需要先完成A的构造
2.4.3.7 static静态成员变量
所有对象都共享同一份数据。
编译阶段就会进行内存分配(全局区)
类内声明,类外进行初始化操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Person { public: static int m_A; } int Person::m_A = 100;
void test() { Person p1; cout<< p1.m_A <<endl; Person p2; p2.m_A=200; cout << p1.m_A << endl; }
|
静态成员变量不属于任何一个对象,所有对象都共享一份数据。
所以可以通过两种方式进行访问:
1 2 3
| Person p1; cout << p1.m_A << endl; cout << Person::m_A <<endl;
|
2.4.3.8 static静态成员函数
所有对象都共享一个函数,并且函数只能访问静态成员变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Person { public: static int m_A; static void func() { cout << "static void func调用" << endl; cout << m_A << endl; } } int Person::m_A = 100;
void test() { Person p1; p1.func(); Person::func(); }
|
2.4.4 C++对象模型和this指针
2.4.4.1
成员变量和成员函数分开存储
注意:静态成员变量或函数不属于任何一个对象上。
空对象占用的内存空间大小为:1
只有非静态成员变量属于类的对象上,
其他(静态成员变量、成员函数)都不属于类的对象上,也就是说只有一份。
2.4.4.2 this指针
由2.4.4.1可知,多个对象会共享(静态成员变量、静态成员函数和非静态成员函数)。
可以通过this指针区分对象调用
this指针指向被调用的成员函数所属的对象
用途:
- 当函数形参和成员变量同名时,可以用this来区分
- 当类的非静态成员函数返回对象本身时,可以用
return *this
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Person { public: string name; void setName(string name) { this->name = name; } Person getInfo() { return *this; } }
|
2.4.5 const修饰成员函数(常函数)
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字
mutable后,在常函数中依然可以修改
对象创建时,加const称为常对象,常对象只能调用常函数。
主要是对函数内this指针的限制
1 2 3 4 5 6 7 8 9 10 11 12
| class Person { public: void showPerson() const { this -> m_A = 100; this -> m_B = 100; } int m_A; mutable int m_B; }
|
2.4.6 友元 friend
让一个函数或者类 访问另一个类中的私有成员
友元的三种实现:
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
| #include <iostream> using namespace std; class Building{ friend void goodGay(Building* building); public: string m_SittingRoom; Building() { m_SittingRoom = "客厅"; m_BedRoom = "卧室"; } private: string m_BedRoom; };
void goodGay(Building *building) { cout << "Gay:" << building->m_SittingRoom << endl; cout << "Gay:" << building->m_BedRoom << endl; } void main() { Building a; goodGay(&a); }
|
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
| #include <iostream> #include <string> using namespace std; class Building { friend class GoodGay; public: string m_SittingRoom; Building() { m_SittingRoom = "客厅"; m_BedRoom = "卧室"; } private: string m_BedRoom; };
class GoodGay { public: Building* building = new Building; void visit(); };
void GoodGay::visit() { cout << "Gay:" << building->m_SittingRoom << endl; cout << "Gay:" << building->m_BedRoom << endl; }
void main() { GoodGay gay; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <iostream> #include <string> using namespace std; class Home; class GoodGay { public: void func(Home* home); }; class Home { friend void GoodGay::func(Home * home); private: int id = 10; };
void GoodGay::func(Home* home) { cout << home->id << endl; } void main() { GoodGay a; Home home; a.func(&home); }
|
2.4.7 运算符重载
对于内置的数据类型,编译器知道怎么进行数据运算
但是对于其他的数据类型,需要自己定义计算方法
2.4.7.1 加号运算重载
通过成员函数重载”+“
1 2 3 4 5 6 7
| 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; }
|
通过全局函数重载”+“
1 2 3 4 5 6 7
| 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; }
|
2.4.7.2 左移运算符重载
作用:自定义输出内容
只能用全局函数进行重载
1 2 3 4 5
| ostream & operator<<(ostream &cout,Person p) { cout << p.m_A; return cout; }
|
2.4.7.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
| #include <iostream> #include <string> using namespace std; class MyInt { friend ostream & operator<<(ostream& out, MyInt& p); public: MyInt &operator++() { i++; return *this; } MyInt operator++(int) { MyInt temp = *this; i++; return temp; } private: string name = "vicczyq"; int i = 10; };
ostream & operator<<(ostream &out, MyInt &p) { out << p.name << p.i; return out; } void main() { MyInt a; cout << a << endl; ++(++a); cout << a << endl; }
|
2.4.7.4 赋值运算符重载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Person { public: int *m_age; Person(int x) { m_age = new int(x); } Person & operator=(Person & p) { if(m_age!=NULL) { delete m_age; m_age = NULL; } m_age = new int(p.m_age); return *this; } }
|
2.4.7.5 关系运算符重载
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Person { public: string name; int id; bool operator==(Person &p) { if(p.name==name && p.id==id) { return true; } else return false; } }
|
2.4.8 继承
下级别成员拥有上一级的共性,又有自己的特点。减少重复代码。
image-20240303191037932
2.4.8.1 继承的基本语法
基本语法:class 子类 : 继承方式 父类
子类 也称为 派生类
父类 也称为 基类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <iostream> #include <string> using namespace std;
class BasePage { public: void Print() { cout << "父类的打印函数" << endl; } }; class Page1 : public BasePage {
}; void main() { Page1 p; p.Print(); }
|
2.4.8.2继承方式
1 2 3 4 5 6 7 8 9
| class A { public: int a; protected: int b; private: int c; };
|
公共继承
1 2 3 4 5 6 7
| class B : public A { public: int a; protected: int b; };
|
保护继承
1 2 3 4 5 6
| class C : protected A { protected: int a; int b; }
|
私有继承
1 2 3 4 5 6
| class D : private A { private: int a; int b; }
|
所谓的继承方式就是继承成为对应权限,除了公共继承需要考虑protected的处理。
注意:父类的private不管怎么样都是不能继承的
2.4.8.3 继承关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class BasePage { public: int a; protected: int b; private: int c; }; class Page1 : public BasePage { }; void main() { Page1 p; cout << sizeof(p) << endl; }
|
父类中所有的非静态成员属性都会被子类继承下去,但是私有的属性被编译器隐藏了。
2.4.8.4 构造和析构顺序
子类继承父类,
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
| #include <iostream> #include <string> using namespace std; class BasePage { public: BasePage() { cout << "父类构造函数" << endl; } ~BasePage() { cout << "父类析构函数" << endl; } }; class Page1 : public BasePage { public: Page1() { cout << "子类构造函数" << endl; } ~Page1() { cout << "子类析构函数" << endl; } }; void main() { Page1 p; }
|
先调用父类的构造函数,再调用子类的构造函数
先调用子类的析构函数,再调用父类的析构函数
image-20240303195651224
2.4.8.5 同名成员处理方式
- 访问子类中的同名成员:直接访问即可
- 访问父类中的同名成员:需要添加作用域
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
| #include <iostream> #include <string> using namespace std;
class BasePage { public: string str="父类"; void func() { cout << "同名成员函数:父类" << endl; } }; class Page1 : public BasePage { public: string str="子类"; void func() { cout << "同名成员函数:子类" << endl; } }; void main() { Page1 p; cout << p.str << endl; cout << p.BasePage::str << endl; p.func(); p.BasePage::func(); }
|
2.4.8.6 多继承语法
语法:`class 子类 : 继承方式 父类1, 继承方式 父类2 ...
多继承可能会引发父类中同名成员的出现,需要加作用域进行区分,
C++实际开发中不建议用多继承
2.4.8.7 菱形继承/虚继承
两个类继承于同一个父类,又有某个类同时集成于这两个类
img
由上图可知,羊驼继承了动物的数据两次,我们真实情况只需要一个数据就行
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Animal{ public: int m_Age; }; class Sheep : public Animal{}; class Tuo : public Animal{}; class SheepTuo : public Sheep, public Tuo{}; void main() { SheepTuo st; st.Sheep::m_Age = 18; st.Tuo::m_Age = 28; }
|
虚继承即可解决
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Animal{ public: int m_Age; }; class Sheep :virtual public Animal{}; class Tuo :virtual public Animal{}; class SheepTuo : public Sheep, public Tuo{}; void main() { SheepTuo st; st.Sheep::m_Age = 18; st.Tuo::m_Age = 28; }
|
2.4.9 多态
对于同一个函数,传入不同的参数会反应出不同的状态,即执行不同的内容。
- 静态多态:函数重载 和 运算符重载
- 动态多态: 子类和虚函数实现运行时的多态
静态多态和动态多态的区别:
- 静态多态:编译阶段确定函数地址(早绑定)
- 动态多态:运行阶段确定函数地址(晚绑定)
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
| class Animal{ public: void speak() { cout<<"动物正在说话"<<endl; } }; class Cat : public Animal{ public: void speak() { cout<<"小猫在说话"<<endl; } };
void doSpeak(Animal & animal) { animal.speak(); } void main() { Cat cat; doSpeak(cat); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Animal{ public: virtual void speak() { cout<<"动物正在说话"<<endl; } }; class Cat : public Animal{ public: void speak() { cout<<"小猫在说话"<<endl; } };
void doSpeak(Animal & animal) { animal.speak(); } void main() { Cat cat; doSpeak(cat); }
|
动态多态满足条件:
- 有继承关系
- 子类要重写父类的虚函数
重写:函数名、参数、返回值类型都必须相同。
重载:函数名相同,参数、返回值类型可能不同。
2.4.9.1多态的原理剖析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Animal { void doSpeak() { cout << "动物正在说话" << endl; } }; 此时sizeof(Animal)为1 class Animal { void virtual doSpeak() { cout << "动物正在说话" << endl; } }; 此时的sizeof(Animal)为4
|
4个字节的原因是指针占用。
virtual关键字会形成一个vfptr指针(虚函数表指针)virtual
function pointer
image-20240304194459436
当子类继承时,会重写doSpeak函数,vfptr的指针指向的地址就会发生改变。
image-20240304195120122
在每次调用时候,会直接调用当前类的函数,所以可以实现多态。
2.4.9.2 纯虚函数和抽象类
通常发生多态后,父类中的函数就没什么意义,主要是用于调用子类重写内容,
因此可以将虚函数改为纯虚函数
语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类就称为抽象类。
抽象类特点:无法实例化对象,子类必须重写抽象类中的纯虚函数,否则也属于抽象类
1 2 3 4 5 6 7 8 9 10 11 12
| class Animal{ public: virtual void doSpeak() = 0; };
class Cat : public Animal{ public: void doSpeak() { cout<<"小猫在说话"<<endl; } };
|
2.4.9.3 虚析构和纯虚析构
多态使用时,如果子类中有开辟到堆区的数据,父类指针在释放时无法调用到子类的析构代码
父类指针在进行析构的时候,不会调用子类中的析构函数,导致子类的堆区数据,出现内存泄漏。
采用 虚析构 和 纯虚析构
即可解决。
1 2 3 4 5 6 7 8 9 10 11 12
| virtual ~类名(){ }
virtual ~类名() = 0;
Animal::~Animal() { }
|
2.5文件操作
C++对文件操作需要包含头文件<fstream>
文本文件以ASCII码形式存储在计算机中,二进制文件以二进制形式存储
三大类:
- ofstream:写操作
- ifstream:读操作
- fstream:读写操作
2.5.1 文本文件
打开方式:
image-20240304204659205
2.5.1.1 写文件
1 2 3 4 5 6 7 8 9
| #include <fstream> int main() { ofstream ofs; ofs.open("文件路径", ios::out); ofs << "写入的数据"; ofs.close(); return 0; }
|
文件打开方式可以配合使用,如ios::out | ios::binary就是以二进制形式写出
2.5.1.2 读文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <fstream> #include <iostream> int main() { ifsteam ifs; ifs.open("文件路径", ios::in); if(!ifs.isopen()) { cout<<"文件打开失败"<<endl; return 0; } ifs.close(); return 0; }
|
读数据:
方法一:
1 2 3 4 5
| char buf[1024] = {0}; while( ifs >> buf ) { cout << buf << endl; }
|
方法二:
1 2 3 4 5
| char buf[1024] = {0}; while( ifs.getline(buf),1024 ) { cout << buf << endl; }
|
方法三:
1 2 3 4 5
| string buf; while( getline(ifs, buf)) { cout << buf; }
|
方法四:
1 2 3 4 5
| char c; while( (c=ifs.get()) != EOF ) { cout << c; }
|
总结
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <fstream> #include <iostream> using namespace std; int main() { ofstream ofs; ofs.open("test.txt", ios::out); ofs << "张三" << endl; ofs << "18岁" << endl; ofs << "男" << endl; ofs.close();
ifstream ifs; ifs.open("test.txt", ios::in); if (!ifs.is_open())return 0; string str; while ( ifs>>str ) { cout << str<<endl; } ifs.close(); return 0; }
|
2.5.2 二进制文件
打开方式需要添加ios::binary
2.5.2.1 写文件
1 2 3 4 5
| Person p = {"张三",18}; ofstream ofs; ofs.open("文件地址", ios::binary | ios::out); ofs.write((const char*)p, sizeof(Person)); ofs.close();
|
2.5.2.1 读文件
1 2 3 4 5 6 7 8
| Person p; ifstream ifs; ifs.open("文件地址", ios::binary | ios::in);
if(!ifs.is_open())return 0;
ifs.read((const char*)p, sizeof(Person)); ifs.close();
|
2.6 模板
2.6.1基本语法
1 2 3 4 5 6 7 8 9 10 11 12
| 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; }
|
数据类型不同,但是代码逻辑是相同的,采用模板可以改写为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| template <typename T>
void Myswap(T &a, T &b) { T temp = a; a = b; b = temp; }
void main() { int a=10,b=20; Myswap(a,b); Myswap<int>(a,b); }
|
模板必须要确定出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
| #include <iostream> using namespace std; template <class T> void Swap_element(T &a,T &b) { T temp; temp = a; a = b; b = temp; } template <class T> void SelectSort(T &arr) { int len = sizeof(arr) / sizeof(arr[0]); for (int i = 0; i < len; i++) { int maxFlag = i; for (int j = i + 1; j < len; j++) { if (arr[j] > arr[maxFlag]) { maxFlag = j; } } if (maxFlag != i) { Swap_element(arr[maxFlag], arr[i]); } } for (int i = 0; i < len; i++) { cout << arr[i] << " "; } }
int main() { int a[] = { 2,3,4,1,0,2,9,10 }; SelectSort(a); char str[] = "gfbseqa"; SelectSort(str); return 0; }
|
2.6.2 普通函数与函数模板的区别
普通函数可以发生隐式类型转换
函数模板(自动类型推导),不可以发生隐式类型转换
函数模板(手动指定类型),可以发生隐式类型转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| int Add_pt(int &a,int &b){ return a+b; }
template <typename T> T Add_mb(T &a, T &b){ return a+b; }
void main(){ int a=1,b=2; char c=3; Add_pt(a,c); Add_mb(a,c); add_mb<int>(a,c); }
|
2.6.3
普通函数和函数模板调用规则
- 如果普通函数和函数模板都可以实现,优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板
- 函数模板页也可以发生重载
- 如果函数模板产生更好的匹配,优先调用函数模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void Print(int a,int b){ cout<<"调用普通函数"<<endl; }
template<class T> void Print(T a,T b){ cout<<"调用模板函数"<<endl; }
template<typename T> void Print(T a,T b,T c){ cout<<"调用重载的函数模板"<<endl; } void main() { Print(1,1); Print<>(1,1); Print(1,1,1); Print('a','b'); }
|
2.6.4 模板局限性
模板并不是万能的,如果传入 a和b是一个数组,模板就无法实现
1 2 3 4
| template <class T> void f(T a, T b){ a = b; }
|
如果传入的是像Person这样的自定义数据类型,也无法正常运行
1 2 3 4
| template <class T> bool f(T a,T b){ if(a > b){.....} }
|
解决方法:1.运算符重载、2.具体化Person
1 2 3 4
| template<> bool f(Person &p1, Person &p2){ if(p1.m_Name==p2.m_Name)return true; else return false; }
|
2.6.5 类模板
类模板中的成员函数只有在调用时候才会进行创建,普通类中的成员函数在一开始就创建在代码区
(可以记成,不同的一组typename就是不同的class,所以在外部访问的时候需要用<>)
2.6.5.1 基本语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <iostream> using namespace std;
template <typename NameType,typename AgeType> class Person { public: NameType m_Name; AgeType m_Age; Person(NameType name, AgeType age){ m_Name = name; m_Age = age; } }; int main() { Person<string,int> p("vicczyq",18); return 0; }
|
2.6.5.2 类模板和函数模板的区别
- 类模板没有自动类型推导的使用方式
- 类模板在木板参数列表中可以有默认参数
1 2 3 4 5 6
| template <typename NameType, typename AgeType =int> class Person{ public: NameType m_Name; AgeType m_Age; };
|
2.6.5.3 类模板对象作函数参数
1 2 3 4 5 6
| template <typename NameType, typename AgeType> class Person{ public: NameType m_Name; AgeType m_Age; };
|
1 2 3 4 5 6 7
| void printPerson(Person<string,int> &p){ } void main(){ Person<string,int> p("vicczyq",18); printPerson(p); }
|
1 2 3 4 5 6 7 8
| template<typename T1, typename T2> void printPerson(Person<T1,T2> &p){ } void main(){ Person<string,int> p("vicczyq",18); printPerson(p); }
|
1 2 3 4 5 6 7 8 9
| template<typename T> void printPerson(T &p){ } void main() { Person<string,int> p("vicczyq",18); printPerson(p); }
|
2.6.5.4 类模板继承
当父类是一个类模板时,子类继承需要指出父类中的数据类型
如果想灵活使用数据类型,子类也必须为类模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| template <typename NameType,typename AgeType> class Person{ public: NameType m_Name; AgeType m_Age; }
class Son : public Person<string,int>{ }
template <typename T1, typename T2, typename T3> class Son : public Person<T1,T2> { public: T3 sex; }
|
2.6.5.5 类模板成员函数类外实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| template <typename NameType> class Person{ public: NameType m_Name; void getName(); Person(); };
template <typename T> T Person<T>::getName() { return m_Name; }
template <typename T> Person<T>::Person(){
}
|
2.6.5.6 类模板份文件编写
由于类模板中成员函数创建时机是在调用阶段,导致份文件编写时候无法链接。
解决方法:
person.h
1 2 3 4 5 6 7
| template <typename NameType> class Person{ public: NameType m_Name; void getName(); Person(); };
|
person.cpp
1 2 3 4 5 6 7 8 9 10 11 12
| #include "person.h"
template <typename T> T Person<T>::getName() { return m_Name; }
template <typename T> Person<T>::Person(){
}
|
main函数
1 2 3 4
| #include "person.cpp" void main(){ Person<string> p; }
|
- 解决方法2:将声明和实现写到同一个文件中,并更改后缀名为
.hpp(.hpp只是约定名称,不是强制限制)相当于直接写在了.h文件中
main函数
1 2 3 4
| #include "person.hpp" void main(){ Person<string> p; }
|
person.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| template <typename NameType> class Person{ public: NameType m_Name; void getName(); Person(); }; template <typename T> T Person<T>::getName() { return m_Name; }
template <typename T> Person<T>::Person(){
}
|
2.6.5.7 类模板与友元
1 2 3 4 5 6 7 8 9 10 11
| template <typename NameType, typename AgeType> class Person{ friend void PrintInfo(Person<NameType,AgeType> &p) { cout << p.m_Name << p.m_Age; } private: NameType m_Name; AgeType m_Age; };
|