4. 控制语句
三目运算符返回的是一个变量,可以继续进行赋值。
(a > b ? a : b) = 100;//就是把两者中比较大的那个的值赋成100
4.1 switch语句
int main(){
int num = 0;
cin >> num;
switch(num)
{
case 1:
cout << 1 << endl;
break;
case 2:
cout << 2 << endl;
break;
default:
cout << "else" <<endl;
break;
}
system("pause");
}
生成随机数,使用rand()函数,没有头文件
#include<ctime>
srand((unsigned int)time(NULL));
int num = rand() % 100 + 1;
5. 数组
5.1 概述
cout << sizeof(arr) / sizeof(arr[0]) << endl;//可以计算出数组的长度
cout << "数组的首地址" << arr <<endl;
cout << "数组的首地址" << &arr[0] <<endl;
//arr = 100; 错误,数组名是常量,因此不可以进行赋值操作
5.2 二维数组
二维数组的数组名:
- 可以查看二位数组占用的内存空间
- 可以查看二位数组的首地址
6. 函数
函数的声明可以有多次,但是定义只能有一次
6.7 函数的分文件编写
函数分文件编写的一般步骤:
- 创建后缀名为.h的头文件
- 创建后缀名为.cpp的源文件
- 在头文件中写函数的声明
- 在源文件中写函数的定义
//add.h
#include<iostream>
using namespace std;
//函数声明
int add(int a, int b);
//add.cpp
#include "add.h"
//函数定义
int add(int a, int b){
return a + b;
}
7. 指针
7.1 指针的基本概念
指针的作用:可以通过指针间接访问内存
- 内存编号是从0开始记录的,一般使用16进制数来表示
- 可以使用指针变量保存地址
在32位操作系统下,指针占4个字节;在64位中,指针占8个字节。
7.4 空指针
空指针:指针变量指向内存中编号为0的空间、
用途:初始化指针变量
注意:空指针指向的内存是不可以访问的
int *p = NULL;
0~255的内存区域是系统保留,不可以访问。
野指针:指针指向非法的内存空间
在程序中要尽量避免野指针
7.5 const修饰指针
const修饰指针由三种情况:
- const修饰指针——常量指针
- const修饰常量——指针常量
- const既修饰指针,又修饰常量
const int * p = &a;//常量指针
//特点是:指针的指向可以改,指针指向的值不可以改
*p = 20;//错误
p = &b;//正确
int* const p = &a;//指针常量
//指针的指向不可以改,但是指针指向的值可以改
*p = 20;//正确
p = &b;//错误
const int* const p = &a;//const既修饰指针,又修饰常量
//指针的指向不可以改,指针指向的值也不可以改
7.6 指针和数组
作用:利用指针访问数组中的元素
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p = arr;//指向数组的指针
for(int i = 0; i < 10 ; i++){
cout << *(p + i) << " ";
}
cout <, endl;
7.7 指针和函数
作用 :利用指针作为函数参数,可以修改实参的值。
//swap函数
void swap(int* a, int* b){
int temp = *a;
*a = *b;
*b = temp;
}//可以实现实参的对换
7.8 指针配合数组和函数案例
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
using namespace std;
void bubbleSort(int* arr, int len){
for(int i = 0; i < len; i++){
for(int j = 0; j < len - i - 1; j++){
if(arr[j] > arr[j+1]){
int temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
}
void printArray(int* arr, int len){
for(int i = 0; i < len; i++){
cout << arr[i] << " ";
}
cout << endl;
}
int main(){
int arr[10] = {3, 4, 0, 5, 2, 8, 7, 6, 1, 9};
int len = sizeof(arr) / sizeof(arr[0]);
printArray(arr, len);
bubbleSort(arr, len);
printArray(arr, len);
system("pause");
return 0;
}
8. 结构体
结构体属于用户自定义的数据类型,允许用户存储不同的数据类型。
8.2 结构体定义和使用
语法:
struct 结构体名 {结构体成员列表};
通过结构体创建变量的方式有三种:
- struct 结构体名 变量名
- struct 结构体名 变量名 = {成员1值, 成员2值 …};
- 定义结构体是顺便创建变量
//结构体定义
struct student{
string name;
int age;
int score;
}studentA;
struct student studentB;
studentB.name = "张三";
struct student studentC = {"a", 18, 88};
//结构体变量创建的时候struct关键字可以省略
8.3 结构体数组
就把结构体类型当作一个新的数据类型来进行操作就可以了。
8.4 结构体指针
//结构体定义
struct student{
string name;
int age;
int score;
}s;
int main(){
student *p = &s;
cout << s.name<<endl;//结构类型变量可以用.来访问属性
cout << p->name <<endl;//对应指针类型的变量要用 -> 来访问属性
}
8.5 结构体嵌套结构体
8.6 结构体做函数参数
- 值传递
- 地址传递
8.7 结构体中const使用场景
作用:用const防止误操作
防止在进行地址传递的时候修改了实参的值
//结构体定义
struct student{
string name;
int age;
int score;
}s;
//常量指针
void printStudent(const student* p){//这里const的意思是不能修改p指向的对象的属性值
cout << p->name << endl;
}
9. 内存分区模型
C++程序在执行时,将内存大方向划分为4个区域
- 代码区:存放函数体的二进制代码,由操作系统进行管理
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值、局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
内存四区的意义:
不同区域存放的数据,赋予不同的声明周期,给我们更大的灵活编程。
9.1 程序运行前
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域:
代码区
存放CPU执行时的机器指令
代码区是共享的,共享的目的是,对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
全局区
全局变量和静态变量存放在此
全局区还包含了常量区,字符串常量和其他常量也存放在此
该区域的数据在程序结束后由操作系统释放
常量包括:字符串常量;const关键字修饰的常量,其中const修饰的全局变量在全局区,而修饰的局部变量并不在
9.2 程序运行后
栈区
由编译器自动分配释放,存放函数的参数值,局部变量等
注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
堆区
由程序员分配释放,若程序员不释放,程序结束由操作系统回收
在C++中主要利用new关键字在堆区开辟内存
9.3 new关键字
释放时使用关键字delete
利用new创建的数据,会返回该数据对应的类型的指针
int* func(){
int* p = new int(10);
return p;
}
int main(){
int *p = func();
delete p;
//在堆区创建一个数组
int* arr = new int[10];
delete[] arr;
}
10. 引用
10.1 引用的基本作用
作用:给变量起别名
语法:
int a = 10;
int &b = a;//b是别名
10.2 引用注意事项
- 引用必须进行初始化
- 引用在初始化后,不可以改变
10.3 引用作函数参数
作用 :函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参
void swap(int& a, int& b){
int temp = a;
a = b;
b = temp;
}
int main(){
int a = 10;
int b = 20;
swap(a, b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
system("pause");
}
10.4 引用做函数函数返回值
作用:引用可以作为函数返回值
注意:不要返回局部变量的引用
用法:函数调用作为左值
//函数调用作为左值
int& test(){
static int a = 10;
return a;
}
int main(){
int &temp = test();
test() = 1000;//相当于将a和temp指代的对象的值修改为了1000
}
//如果函数的返回值是引用,这个函数调用可以作为左值
10.5 引用的本质
本质:引用的本质在C++内部实现是一个指针常量
int a = 10;
int& ref = a;
//编译器自动转换为 int* const ref = &a;指针常量的指针指向不可以修改,也说明为什么引用不可修改
ref = 20;//内存变量ref是引用,自动转换为: *ref = 20;
10.6 常量引用
作用:常量引用主要用来修饰形参,防止误操作
在函数形参列表中,可以加const修饰形参,防止形参改变实参
int& ref = 10;//错误,引用必须指向一块合法的内存区域
const int& ref = 10;//正确,编译器将代码修改为,int temp = 10;const int& ref = temp;
11. 函数提高
11.1 函数默认参数
在C++中,函数的形参列表是可以有默认值的
默认参数放在最后
如果函数的声明有默认参数,函数实现就不能有默认参数
11.2 函数占位参数
C++函数的形参列表可以有占位参数,调用函数的时必需填补该位置
语法:返回值类型 函数名(数据类型){}
void func(int a, int){
cout << "null" << endl;
}
int main(){
func(10, 10);
}
11.3 函数重载
作用:函数名可以重用,提高复用性
函数重载满足条件:
- 同一个作用域下
- 函数名称相同
- 函数参数类型不同,或者个数不同,或者顺序不同
注意:函数的返回值不同不能作为函数重载的条件
注意事项
-
引用作为重载的注意事项
void func(int &a){ cout << "func()" << endl; } void func(const int &a){ cout << "func2()" <<endl; } int main(){ int a = 10; func(a);//这里调用的是不带const的那个,a是一个可读可写的变量,更倾向于不带const的 func(10);//这里调用的是带const的那个,因为 int &a = 10;是错误的 }
-
函数重载遇到默认参数
void func(int a, int b = 10){ cout << 1 << endl; } void func(int a){ cout << 2 << endl; } //上面两个函数在编译的时候不会出错 int main(){ func(10);//但是在执行这个调用的时候编译器会不知道执行哪个函数,所以会出错 }
12. 类和对象
C++面向对象的三大特性:封装、多态、继承
C++认为万物皆对象,对象上有属性和行为
12.1 封装
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
权限
- public 共有权限 类内可以访问,类外也可以访问
- protected 保护权限 类内可以访问,类外不可以访问
- private 私有权限 类内可以访问,类外不可以访问
子类可以访问父类的保护内容,但不能访问父类的私有内容
struct的默认权限是公共权限
class的默认权限是私有权限
成员属性设置为私有
优点1:将所有成员属性设置为私有,可以自己控制读写权限
优点2:对于写权限,我们可以检测数据的有效性
可以将类的声明和实现放在不同的类中
//point.h
#pragma once
#include<iostream>
using namespace std;
class Point{
public:
void setX(int x);
void setY(int y);
int getX();
int getY();
private:
int p_X;
int p_Y;
}
//使用这个类的时候只要包含 #include "point.h"
//point.cpp
#include "point.h"
#include<iostream>
using namespace std;
void Point::setX(int x){
p_X = x;
}
void Point::setY(int y){
p_Y = y;
}
int Point::getX(){
return p_X;
}
int Point::getY(){
return p_Y;
}
12.2 对象的初始化和清理
12.2.1 构造函数和析构函数
对象的初始化和清理是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后结果也是未知
同样的使用完一个对象和变量,没有及时清理也会造成一定的安全问题
类的构造函数和析构函数用于完成上述工作,这两个函数将会被编译器自动调用,完成对象初始化和清理工作
编译器提供的构造函数和析构函数是空实现
- 构造函数:主要作用在于创建对象时为对象成员属性赋值
- 析构函数:主要作用在于对象销毁前系统自动调用
12.2.2 构造函数的分类及调用
两种分类方式:
按参数分为:有参构造和无参构造
按类型分为:普通构造和拷贝构造
三种调用方式:
括号法:
显示法
隐式转换法
//拷贝构造函数
Person(const Person& p){
age = p.age;
}
Person p1();//不会调用默认构造函数,编译器会以为这是一个函数声明
//显示法
Person p2 = Person(10);
Person(10);//匿名对象,当前行执行结束后,系统会立即回收匿名对象
//不要使用拷贝构造函数来初始化匿名对象
Person(p3);//编译器会认为是 Person p3;
//隐式转换法
Person p4 = 4;//等价于 Person p4 = Person(4);
12.2.3 拷贝构造函数调用时机
C++中拷贝构造函数调用时通常有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
12.2.4 构造函数调用规则
12.2.5 深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
class Person{
private:
int m_Age;
int* m_Height;
//拷贝构造函数
Person(const Person & p){
m_Age = p.m_Age;
//m_Height = p.m_Height;编译器默认实现的浅拷贝操作
m_Height = new int(*p.m_Height); //深拷贝操作
//在堆区中新申请了一块内存,用于存放相关数据
}
}
//析构代码
if(m_Height != NULL){
delete m_Height;
m_Height == NULL;
}
12.2.6 初始化列表
作用:
C++提供了初始化列表语法,用来初始化属性
class Person{
public:
int m_A;
int m_B;
int m_C;
//使用初始化列表初始化属性
Person(int a, int b, int c): m_A(a), m_B(b), m_C(c) {}
}
12.2.7 类对象作为类成员
C++类中的成员可以是类一个类的成员
class A{
public:
int m_age;
}
class B{
public:
A a;
};
//创建B类的对象时,会先调用A中的构造函数,然后再调用B类的构造函数
//析构的顺序与构造相反
12.2.8 静态成员
静态成员就是在成员变量和成员函数前加上传剪子static,成为静态成员
静态成员变量要类内声明,类外进行初始化
静态成员分为:
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有对象共享同一个函数\
- 静态成员函数只能访问静态成员变量
//有两种访问方式
class Person{
public:
static int m_B;
static void func(){
cout << "静态成员函数的调用" << endl;
}
//静态成员函数只能访问静态成员变量
//静态成员函数也是有访问权限的,类外访问不到私有的静态成员函数
}
int Person::m_B = 0;//类外初始化
void test(){
//1.通过对象来进行调用
Person p;
p.func();
//2.通过类名来进行调用
Person::func();
}
12.3 C++对象模型和this指针
12.3.1 成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
空对象占用内存空间为1
C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置
每个空对象也应该有一个独一无二的内存地址
12.3.2 this指针概念
通过12.3.1我们知道C++中成员变量和成员函是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这一块代码是如何区分是哪个对象调用自己的呢?
C++通过提供特殊的对象指针:this指针,解决上述问题:this指针指向被调用的成员函数所属的对象
this指针是隐含在每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的用途:
- 当形参和成员变量同名时,可以用this指针来进行区分
- 在类的非静态成员函数中返回对象本身,可使用 return *this;
class Person{
public:
int age;
Person(int age){
this -> age = age;
}
}//用于区分同名变量
class Person{
public:
int age;
Person& PersonAddAge(const Person & p){
//这里要返回引用,而不是值,如果是值的话相当于在返回的时候调用拷贝构造函数,链式之后的
//处理都是在新对象上的,而不是在原来的p2
this -> age += p.age;
return *this;
}
}
int main(){
Person p1(10);
Person p2(10);
//链式编程思想
p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);
//在类的非静态成员中返回对象本身
}
12.3.3 空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性
if(this == NULL){
return;
}//添加到用到this指针的函数中,防止空指针调用时程序崩溃
12.3.4 const修饰成员函数
常函数:
- 成员函数后加const我们称这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加mutable关键字,在常函数中依然可以改变
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
class Person{
public:
int m_A = 0;
mutable int m_B = 0;
//this指针的本质是一个指针常量,指针的指向是不可以修改的
//Person * const this. this指针就相当于是前面的那种声明
//如果函数声明时再加一个const,那么this指针指向的值也就不能修改了
void showPerson() const{
//m_A = 100;错误
m_B = 100;
}
}
const Person p;//常对象
p.m_A = 100;//错误
P.m_B = 100;//正确
//常对象只能调用常函数
p.showPerson();
12.4 友元
12.4.1 全局函数做友元
class Building{
//声明友元
friend void goodGay(Building *building);
private:
string bedroom;
};
//本来在类外是不能访问类的私有属性的
//但是将全局函数声明为类的友元有以后,就可以访问类的私有属性了
void goodGay(Building *building){
cout << building -> bedroom <<endl;
}
12.4.2 类做友元
friend class goodGay;
12.4.3 成员函数做友元
friend void goodGay::visit();
12.5 运算符重载
12.5.1 加号运算符重载
可以通过成员函数来进行重载+,也可以通过全局函数来重载 +
运算符重载也可以发生函数重载
对于内置数据类型不能进行运算符重载
class Person{
public:
int m_A;
int m_B;
//通过成员函数来重载加号
Person operator+(const Person & p) const;
}
Person Person::operator+(const Person & p) const{
Person temp;
temp.m_A = this -> m_A + p.m_A;
temp.m_B = this -> m_B + p.m_B;
return temp;
}
//类外调用
Person p1;
p1.m_A = 10;
p1.m_B = 10;
Person p2;
p2.m_B = 10;
p2.m_A = 10;
Person p3 = p1 + p2;
//成员函数的本质调用
Person p3 = p1.operator(p2);
//通过全局函数来重载加号
Person operator+(const Person & p1, const 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 p3 = p1 + p2;
//全局函数的本质调用
Person p3 = operator+(p1, p2);
12.5.2 左移运算符重载
作用:可以输出自定义数据类型
通常不会利用成员函数来重载左移运算符,因为无法实现cout在左侧
//只能利用全局函数重载左移运算符
class Person{
friend ostream& operator<<(ostream &cout, Person &p);
public:
int m_A;
void setB(int b){
this -> m_B = b;
}
private:
int m_B;
}
//重载运算符
//如果要输出其中的私有变量的话,就要将这个函数添加为友元
ostream& operator<<(ostream &cout, Person &p){
cout << "m_A = " << m_A << "m_B = " << m_B << endl;
return cout;
}
//调用
Person p;
p.m_A = 10;
p.setB(10);
cout << p << endl;
12.5.3 递增运算符重载
作用:通过重载递增运算符,实现自己的整型数据
递增包括前置递增和后置递增
前置递增返回的是引用,后置递增返回的是值
class MyInteger{
friend ostream& operator<<(ostream &cout, MyInteger a);
public:
MyInteger(){
this -> num = 0;
}
//实现前置++运算符重载
//返回引用是为了一直对一个数据进行递增
MyInteger& operator++(){
this -> num++;
return *this;
}
//实现后置++运算符重载
//这里要使用占位符来区分前置递增和后置递增
//后置递增不返回引用,因为不能返回局部对象的引用
MyInteger operator++(int){
//先记录当时结果
MyInteger temp = *this;
//再执行++操作
num++;
//最后返回记录的结果
return temp;
}
private:
int num;
};
ostream& operator<<(ostream &cout, MyInteger a){
cout << a.num;
return cout;
}
void test(){
MyInteger a;
//测试前置递增
cout << ++(++a) <<endl;
//测试后置递增
cout << a++ <<endl;
cout << a <<endl;
}
int main(){
test();
system("pause");
}
12.5.4 赋值运算符重载
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
using namespace std;
class Person{
public:
//构造函数
Person(int age){
m_Age = new int(age);
}
//析构函数
~Person(){
if(m_Age != NULL){
delete m_Age;
m_Age = NULL;
}
}
//默认提供的=的重载是值拷贝(浅拷贝操作)
//要用深拷贝重新实现赋值运算符的重载
Person& operator=(Person &p){
//编译器默认提供的是浅拷贝操作
//this -> m_Age = p.m_Age;
//先释放当前m_Age中的内容,再进行深拷贝操作
if(this -> m_Age != NULL){
delete this -> m_Age;
this -> m_Age = NULL;
}
//执行深拷贝操作
this -> m_Age = new int(*p.m_Age);
return *this;
//赋值运算符也要支持链式编程
}
int *m_Age;
};
void test(){
Person p1(18);
Person p2(20);
Person p3(22);
p3 = p1 = p2;
cout << "p1的年龄为: " << *p1.m_Age <<endl;
cout << "p2的年龄为: " << *p2.m_Age <<endl;
cout << "p3的年龄为: " << *p3.m_Age <<endl;
}
int main(){
test();
system("pause");
}
12.5.5 关系运算符重载
作用:重载关系运算符,可以让两个自定义类型进行对比操作
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
using namespace std;
class Person{
public:
int m_Age;
Person(){}
Person(int age){
m_Age = age;
}
//重载关系运算符
bool operator==(const Person &p){
if(this -> m_Age == p.m_Age)
return true;
return false;
}
bool operator!=(const Person &p){
if(this -> m_Age != p.m_Age)
return true;
return false;
}
bool operator>=(const Person &p){
if(this -> m_Age >= p.m_Age)
return true;
return false;
}
bool operator<=(const Person &p){
if(this -> m_Age <= p.m_Age)
return true;
return false;
}
bool operator>(const Person &p){
if(this -> m_Age > p.m_Age)
return true;
return false;
}
bool operator<(const Person &p){
if(this -> m_Age < p.m_Age)
return true;
return false;
}
};
void test(){
Person p1(18);
Person p2(20);
if(p1 == p2){
cout << "相等" <<endl;
}
if(p1 != p2){
cout << "不相等" << endl;
}
}
int main(){
test();
system("pause");
}
12.5.6 函数调用运算符重载
- 函数调用运算符()也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
class MyPrint{
public:
void operator()(string test){
cout << test << endl;
}
};
void test(){
MyPrint myprint;
myprint("hello world");
//非常像一个函数调用
//匿名函数对象,MyPrint()是一个匿名对象,然后又重载了()
MyPrint()("hello");
}
int main(){
test();
system("pause");
}
12.6 继承
继承是面向对象三大特性之一
//写法
class A{
};
class B: public A{
};
//继承的好处:减少重复的代码
12.6.2 继承方式
继承方式一共有3种:
- 公共继承
- 保护继承
- 私有继承
12.6.3 继承对象模型
问题:从父类继承过来的成员,哪些属于子类对象中?
class Base{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son: public Base{
public:
int m_D;
};
void test(){
Son son;
cout << "sizeof son = " << sizeof(son) << endl;
//输出结果为16
//说明所有非静态属性都在子类对象中存放,但是父类私有属性被设置成了不可见
}
int main(){
test();
system("pause");
}
12.6.4 继承中构造和析构的顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数
问题:父类和子类的构造和析构顺序是谁先谁后?
构造顺序:先父类后子类
析构顺序正好相反
栈区先进后出?
12.6.5 继承中同名成员处理方式
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
class Base{
public:
int m_A;
Base(){
m_A = 100;
}
};
class Son: public Base{
public:
int m_A;
Son(){
m_A = 200;
}
};
void test(){
Son son;
cout << "Son 中 m_A = " << son.m_A << endl;
cout << "Base 中 m_A = " << son.Base::m_A <<endl;//访问父类中同名元素需要添加作用域
}
int main(){
test();
system("pause");
}
12.6.6 继承同名静态成员处理方式
静态成员和非静态成员出现同名,处理防止一致
//父类同名静态属性通过子类类名访问
//第一个::表示通过类名的方式来进行访问,第二个::表示作用域
cout << "Base 下 m_A = " << Son::Base::m_A << endl;
12.6.7 多继承语法
C++允许一个类继承多个类
多继承可能会引发父类中有同名成员出现,需要加作用域加以区分
C++实际开发中并不建议使用多继承
多继承中间用逗号隔开
12.6.8 菱形继承
菱形继承导致数据有两份,资源浪费
底层实现回头再看视频
class A{
public:
int m_A;
};
//采用虚继承的方式可以解决菱形继承中数据冗余的问题
class B: virtual public A{};
class C: virtual public A{};
class D: public B, public C{};
void test(){
D d;
//加上虚继承以后,下面三种方式访问的都是同一块内存
d.B::m_A = 10;
d.C::m_A = 20;
cout << d.m_A << endl;
}
int main(){
test();
system("pause");
}
12.7 多态
12.7.1 多态的基本概念
多态是C++面向对象三大特性之一
多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,服用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
class Animal{
public:
void speak(){
cout << "动物在说话" << endl;
}
};
class Cat: public Animal{
public:
void speak(){
cout << "猫在说话" << endl;
}
};
void doSpeak(Animal &animal){
animal.speak();
}
void test(){
Cat cat;
doSpeak(cat);
//这时候输出的结果是“动物在说话”,因为静态多态是早绑定,在编译阶段就确定了函数地址
//在编译阶段,传入的参数类型是Animal,所以会执行Animal类中的speak函数
}
int main(){
test();
system("pause");
}
//虚函数,地址晚绑定
public:
virtual void speak(){
cout << "动物在说话" << endl;
}
};
public:
virtual void speak(){
cout << "猫在说话" << endl;
}
//之后的代码不变,这种情况下就是动态多态,地址晚绑定
//动态多态满足条件
//要有继承关系
//子类要重写父类的虚函数
//动态多态实现
//父类的指针或引用指向子类
重写:函数返回值类型,函数名,参数列表要完全相同
12.7.2 多态的原理剖析
多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
开闭原则:对扩展进行开放,对修改进行闭合
12.7.3 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
12.7.4 虚析构和纯虚析构
问题:在动态多态里,是将父类的指针指向了子类的对象,所以在delete父类指针的时候,并不会调用子类的析构函数。
解决方法:使用虚析构或者纯虚析构
虚析构和纯虚析构函数都需要实现
可以在类外使用 类名::~类名(){}来实现
最后组装电脑的例子,自己抽时间还是要回顾一下
13.文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放
通过文件可以将数据持久化
C++中对文件操作需要包含头文件**#include**
文件类型分为两种:
- 文本文件:文本文件以ASCII码形式存储在计算机中
- 二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
- ofstream:写操作
- ifstream:读操作
- fstream:读写操作
13.1 文本文件
13.1.1 写文件
文件打开方式:
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<fstream>
using namespace std;
void test(){
//1.包含头文件 fstream
//2.创建文件流对象,例如要写入文件
ofstream ofs;
//3.打开文件
ofs.open("test.txt", ios::out);
//默认是放在与cpp文件同目录下
//4.写内容
ofs << "姓名:张三" << endl;
ofs << "性别:男" << endl;
ofs << "年龄:20" << endl;
//5.关闭文件
ofs.close();
}
int main(){
test();
system("pause");
}
13.1.2 读文件
读文件与写文件步骤类似,但是读取方式相对比较多
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<string>
#include<fstream>
using namespace std;
//文本文件 读文件
void test(){
//1.包含头文件 fstream
//2.创建文件流对象,例如要写入文件
ifstream ifs;
//3.打开文件,并判断文件是否打开成功
ifs.open("test.txt", ios::in);
//默认是放在与cpp文件同目录下
if(!ifs.is_open()){
cout << "文件打开失败" << endl;
return;
}
//4.读内容
//一个文件被打开后,只能读取一次
//第一种,将文件中的内容一行行的读取到buff中
char buff[1024] = {0};
while(ifs >> buff){
cout << buff << endl;
}
//第二种,
char buff2[1024] = {0};
while(ifs.getline(buff2, sizeof(buff2))){
cout << buff2 <<endl;
}
//第三种
string buff3;
while(getline(ifs, buff3)){
cout << buff3 << endl;
}
//第四种
char c;
while((c = ifs.get()) != EOF){
cout << c;
//一个一个地读取字符,不常用
}
//5.关闭文件
ifs.close();
}
int main(){
test();
system("pause");
}
13.2 二进制文件
13.2.1 写文件
二进制方式写文件主要利用流对象成员函数write
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<string>
#include<fstream>
using namespace std;
class Person{
public:
char m_Name[50];
int m_Age;
};
//二进制文件 写文件
void test(){
//1.包含头文件
//2.创建文件流对象
ofstream ofs;
//3.打开文件
ofs.open("person.txt", ios::out | ios::binary);
//4.写文件
Person p = {"张三", 18};
ofs.write((const char*)&p, sizeof(p));
//5.关闭文件
ofs.close();
}
int main(){
test();
system("pause");
}
13.2.2 读文件
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<string>
#include<fstream>
using namespace std;
class Person{
public:
char m_Name[50];
int m_Age;
};
//二进制文件 写文件
void test(){
//1.包含头文件
//2.创建文件流对象
ifstream ifs;
//3.打开文件
ifs.open("person.txt", ios::out | ios::binary);
if(!ifs.is_open()){
cout << "文件打开失败" << endl;
return;
}
//4.写文件
Person p;
ifs.read((char*)&p, sizeof(Person));
//重点
cout << "姓名: " << p.m_Name << " " << "年龄: " << p.m_Age << endl;
//5.关闭文件
ifs.close();
}
int main(){
test();
system("pause");
}