C++核心编程
本阶段主要针对C++面向对象编程技术,C++中的核心和精髓。
1. 内存分区模型
C++程序在执行时,将内存大方向分为4个区域:
- 代码区:存放函数体的二进制代码,由操作系统进行管理
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
内存四区意义:
不同区域存放不同的数据,赋予不同的生命周期,给我们更大的灵活编程。
1.1 程序运行前
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域:
代码区:
- 存放CPU执行的二进制机器指令
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可(比如点击微信图标,能够打开多个微信,但是微信的代码在内存中只存在一份)
- 代码区是只读的,使其只读的原因是防止程序意外的修改了它的指令(比如程序启动后,程序代码只读不可修改)
全局区:
- 全局变量和静态变量存放在此
- 全局区还包含了常量区,字符串常量和其它常量也存放于此
-
该区域数据在程序结束后由操作系统释放
1.2 程序运行后
栈区:
- 由编译器自动分配释放,存放函数的参数值,局部变量等
- 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
堆区:
- 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
- 在C++中主要利用new在堆区开辟内存
1.3 new操作符
C++中利用new操作符在堆区开辟数据
堆区开辟数据,由程序员手动开辟,手动释放,释放利用操作符delete
Examples1:
int* getInt(){//在堆区创建int数据//new返回的是该数据的指针return new int (100);
}int main(){int* intPointer = getInt();cout << *intPointer << endl;delete intPointer;//内存已经释放,再次访问即为非法访问cout << *intPointer << endl;return 0;
}
Example2:开辟数组
int* getArray(){return new int[10];
}int main(){int* array = getArray();for (int i = 0; i < 10; ++i) {array[i] = 100 + i + 1;}for (int i = 0; i < 10; ++i) {cout << array[i] << " ";}delete[] array;return 0;
}
2. 引用
2.1 引用的基本使用
作用:给变量起别名
语法:
数据类型 &别名 = 原名
Example:
#include <iostream>
using namespace std;int main(){int a = 10;//给a起别名bint &b = a;cout << b << endl;b = 20;cout << a << endl;return 0;
}
2.2 引用的注意事项
- 引用必须要初始化
- 引用一旦初始化后,就不可以更改了(也就是不能再改变成别的变量的别名了)
2.3 引用做函数参数
作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参
总结:通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单
#include <iostream>
using namespace std;//值传递
void swapFun1(int a, int b){int temp;temp = a;a = b;b = temp;
}//地址传递
void swapFun2(int* a, int* b){int temp;temp = *a;*a = *b;*b = temp;
}//引用传递
void swapFun3(int& a, int& b){int temp;temp = a;a = b;b = temp;
}int main(){//值传递int a = 10;int b = 20;swapFun1(a, b);cout << "a = " << a << ", b = " << b << endl;//地址传递a = 10;b = 20;swapFun2(&a, &b);cout << "a = " << a << ", b = " << b << endl;//引用传递a = 10;b = 20;swapFun3(a, b);cout << "a = " << a << ", b = " << b << endl;return 0;
}
2.4 引用做函数返回值
作用:引用是可以作为函数的返回值存在的
注意:不要返回局部变量引用
用法:函数调用作为左值
Example1:局部变量不能返回引用,因为其生命周期在函数结束中即结束,没有权限再对该块内存进行操作
Example2:静态变量生命周期长,函数结束时任然能够对该块内存进行操作
Example3:如果函数返回的是引用,这个函数调用可以作为左值
int main(){int& ref = testReturnQuoteFun1();cout << "ref = " << ref << endl; //ref = 100cout << "ref = " << ref << endl; //ref = 100testReturnQuoteFun1() = 1000;cout << "ref = " << ref << endl; //ref = 1000cout << "ref = " << ref << endl; //ref = 1000
}
2.5 引用的本质
本质:引用的本质在C++内部实现是一个指针常量
2.6 常量引用
作用:常量引用主要用来修饰形参,防止误操作
在函数形参列表中,可以加const来修饰形参,防止形参改变实参
Example1:
int main(){//错误,不允许该写法int& ref = 10; /*正确编译器将代码进行了修改如下int temp = 10;const int& ref = temp;*/const int& ref = 10; return 0;
}
Example2:
void showFun(const int& a){//a = 100; 错误代码,不可修改cout << "a = " << a << endl;
}
int main(){int a = 10;showFun(a);return 0;
}
3. 函数提高
3.1 函数默认参数
在C++中,函数的形参列表中的形参是可以有默认值的。
语法:
返回值类型 函数名 (参数= 默认值){}
注意:
- 当在函数形参列表中使用默认参数时,默认参数右边的形参也必须是默认参数形式
- 函数声明和函数实现只能有一个有默认参数
Example:
int fun(int a, int b = 20, int c = 30){return a + b + c;
}int main(){cout << fun(10) << endl;return 0;
}
3.2 函数占位参数
C++中函数的形参列表可以有占位参数,用来做占位,调用函数时必须填补该位置,同时占位参数也可以有默认参数
语法:
返回值类型 函数名(数据类型){}
Example1:
void fun(int a, int){cout << "fun" << endl;
}
int main(){fun(10, 20);
}
Example2:
void fun(int a, int = 10){cout << "fun" << endl;
}
int main(){fun(10);
}
3.3 函数重载
3.3.1 概述
作用:函数名可以相同,提高复用性
函数重载需要满足的条件:
- 同一个作用域下
- 函数名称相同
- 函数参数 类型不同 或者 个数不同 或者 顺序不同
注意:函数的返回值不可以作为函数重载的条件
Example:
void fun(){cout << "fun" << endl;
}
void fun(int a){cout << "fun" << endl;
}
void fun(double a){cout << "fun" << endl;
}
void fun(int a, double b){cout << "fun" << endl;
}
void fun(double b, int a){cout << "fun" << endl;
}
int main(){fun();return 0;
}
3.3.2 函数重载注意事项
- 引用作为重载条件
- 函数重载碰到函数默认参数
Example1:
void fun(int& a){cout << "调用fun(int& a)" << endl;
}
void fun(const int& a){cout << "调用fun(const int& a)" << endl;
}
int main(){int a = 10;fun(a); //调用fun(int& a)fun(10); //调用fun(const int& a)return 0;
}
Example2:
void fun(int a, int b = 10){cout << "fun(int a, int b = 10)" << endl;
}
void fun(int a){cout << "fun(int a)" << endl;
}
int main(){//fun(10); 错误,不知道该调用哪一个fun(10, 20);return 0;
}
4. 类和对象
C++面向对象三大特性:封装、继承、多态
C++认为万事万物皆为对象,对象中有其对应的属性和行为
4.1 封装
类在设计时,可以把属性和行为放在不同的权限下,加以控制。访问权限有三种:
- public 公共权限(成员在类内可以访问,类外也可以访问)
- protected 保护权限(成员在类内可以访问,类外不可以访问,子类可以访问)
- private 私有权限(成员在类内可以访问,类外不可以访问,子类不可以访问)
4.1.1 Class和Struct的区别
在C++中,struct和Class没有什么太大的区别,唯一的区别是成员的访问权限不同:
- struct默认权限为公共
- Class默认权限为私有
Example:
#include<iostream>
using namespace std;class C{int a;
};struct S{int a;
};int main(){C c;c.a = 100; //报错,不允许访问S s;s.a = 101; //不报错,允许访问return 0;
}
4.1.2 成员属性设置为私有
将类中成员属性设置为私有属性,优点如下:
- 将所有成员属性设置为私有,可以自己控制读写权限
- 对于写权限,我们可以检测数据的有效性
总结:隐藏属性,暴露方法
Example:
#include<iostream>
#inclde<string>
using namespace std;class Person{
private:string name;int age;string lover;public:void setName(string name){name = name;}string getName(){return name;}void setAge(int age){if(age <= 0){cout << "年纪必须大于0岁!" << endl;return;}age = age;}int getAge(){return age;}
};
4.2 将类写在库函数中
按照如下步骤:
- 在Student.h中对Student类进行声明,注意在声明时,成员函数只声明即可,不需要实现
#ifndef CPLUSSTUDY_STUDENT_H
#define CPLUSSTUDY_STUDENT_H#pragma once
#include <string>
using namespace std;class Student{
private:string name;
public:void setName(const string& input_name);string getName();
};#endif //CPLUSSTUDY_STUDENT_H
- 在Student.cpp中实现
#include "Student.h"void Student::setName(const string& input_name){name = input_name;
}
string Student::getName(){return name;
}
- 在需要使用的cpp中调用
#include <iostream>
#include <string>
#include "Student.h"using namespace std;int main(){Student student;student.setName("AirCL");cout << "name:" << student.getName() << endl;return 0;
}
4.3 对象的初始化和清理
- 生活中我们买的电子产品都会有出场设置,在不用的时候会删除信息以保证自己信息数据的安全
- C++中面向对象来源于生活,每个对象也都会有初始设置以及对象销毁前的清理数据的设置
4.3.1 构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题,对于一个对象或者变量来说,没有初始就对其使用的后果是未知的,同样的,使用完一个对象或者变量后,没有及时清理,也会造成一定的安全问题。
C++利用了构造函数和析构函数解决了上述问题,这两个函数会编译器自动调用,完成对象初始化和清理工作,对象的初始化和清理工作是编译器强制要求做的事情,因此如果我们不提供构造函数和析构函数,编译器会提供空实现的构造函数和析构函数。
- 构造函数:主要用于在创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用
- 析构函数:主要用于对象销毁前系统的自动调用,执行一些清理工作
构造函数语法:
ClassName(){
}
- 构造函数,没有返回值也不用写void
- 函数名与类名相同
- 构造函数可以有参数,因此可以重载
- 程序在调用对象时候会自动调用构造函数,无须手动调用,而且只会调用一次
析构函数语法:
~ClassName(){
}
- 析构函数,没有返回值也不用写void
- 函数名称和类名相同,在名称前加符号 ~
- 析构函数不可以有参数,因此不可以重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
Example:
#include <iostream>
#include <string>
using namespace std;class Duck{
private:string duck_name;
public://无参构造Duck(){cout << "无参构造函数" << endl;}//有参构造Duck(string input_duck_name){duck_name = input_duck_name;cout << "有参构造函数,初始化duck_name = " << duck_name << endl;}//析构函数~Duck(){cout << "析构函数" << endl;}};int main(){/*** 有参构造调用方式:* 括号法:Duck duck("a yellow duck");* 显示法:Duck duck = Duck("a yellow duck");* 隐式法:Duck duck = {"name"};*/Duck duck = Duck();return 0;
}
4.3.2 构造函数的分类
构造函数大体上分为两类:普通构造函数、拷贝构造函数
普通构造函数:有参构造函数、无参构造函数
拷贝构造函数:用来将现有类的全部属性拷贝到即将初始化的类中,传入参数是一个类
Example:
#include <iostream>
#include <string>
using namespace std;class Duck{
private:string duck_name;
public://无参构造Duck(){cout << "无参构造函数" << endl;}//有参构造Duck(const string& input_duck_name){duck_name = input_duck_name;cout << "有参构造函数,初始化duck_name = " << duck_name << endl;}//拷贝构造Duck(const Duck& source_duck){duck_name = source_duck.duck_name;cout << "拷贝构造函数,初始化duck_name = " << duck_name << endl;}//析构函数~Duck(){cout << "析构函数" << endl;}};int main(){Duck source_duck = Duck("source_duck_name");Duck target_duck = Duck(source_duck);return 0;
}
4.3.3 构造函数的调用
构造函数有3种调用方式:
- 括号法
//无参
Duck duck;//有参
Duck duck("a yellow duck");
注意:不能写成Duck duck();,这样会被编译器当作是一个函数声明而无法创建对象
- 显式法
Duck duck = Duck("a yellow duck");
注意:Duck("a yellow duck")被叫做匿名对象,当前行执行完后系统会立即回收匿名对象。同时,不能使用拷贝构造函数来初始化匿名对象,因为编译器会认为该操作是对拷贝构造函数参数对象的重定义
- 隐式法
Duck duck = {"name"}; //若有多个参数,即{参数1,参数2,...}
4.3.4 拷贝构造函数的调用时机
C++中拷贝构造函数调用时机通常由三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
void test_fun(Duck temp_duck){
}int main(){Duck duck = Duck("the name of the duck");test_fun(duck);return 0;
}
- 以值方式返回对象
注意:在新版本C++中,减少了拷贝构造函数的使用次数,因此在函数中直接返回对象不会调用拷贝构造函数,只有主动返回使用拷贝构造函数的对象才行
Duck test_fun(){Duck temp_duck("name1");return Duck(temp_duck);
}
4.3.5 构造函数调用规则
默认情况下,C++编译器至少会给一个类添加3个函数(即使自己不写也会默认添加):
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,C++不再提供默认无参构造函数,但是会提供默认拷贝构造函数
- 如果用户定义拷贝构造函数,C++不会再提供其它构造函数
4.3.6 深拷贝与浅拷贝
深拷贝与浅拷贝是面试经典问题,也是常见的一个坑!
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
注意:在一个类定义时如果没有自定义其拷贝构造函数,那么它会自动生成一个默认的拷贝构造函数,但是这个构造函数比较简单,其内部也是使用的浅拷贝对对象的属性进行的赋值,这样的话如果属性存在指针的情况,默认的拷贝构造函数就会把两个对象的属性指针指向堆区的同一空间,若后续其中一个对象释放了内存,那么另一个在对该块内存进行释放时就会出现问题,因此但凡类中定义有指针属性,就要重写拷贝构造函数,对指针属性进行深拷贝(理论上确实是这样,但是我在CLion中测试发现不重写拷贝构造函数也不会出现问题)
#include "iostream"class People {
public:int age;int* height;public:People() {std::cout << "无参构造函数" << std::endl;}People(int in_age, int in_height){std::cout << "有参构造函数" << std::endl;age = in_age;height = new int(in_height);}People(const People & p){std::cout << "拷贝构造函数" << std::endl;//深拷贝age = p.age;height = new int(*p.height);}~People(){std::cout << "析构函数" << std::endl;if (height != nullptr){delete height;height = nullptr;}}
};int main() {People p1 = People(18, 176);std::cout << p1.age << ", " << *p1.height << std::endl;People p2 = People(p1);std::cout << p2.age << ", " << *p2.height << std::endl;return 0;
}
4.3.7 初始化列表
作用:C++提供了初始化列表语法,用来初始化属性。
语法:
构造函数():属性1(值1), 属性2(值2) ...{
}
Example1:
#include "iostream"class People{
public:int age;int height;int money;public:People():age(10), height(20), money(30){std::cout << "有参构造函数" << std::endl;}
};int main(){People p = People();std::cout << p.age << ", " << p.height << ", " << p.money << std::endl;return 0;
}
Example2:
#include "iostream"class People{
public:int age;int height;int money;public:People(){std::cout << "无参构造函数" << std::endl;}People(int in_age, int in_height, int in_money):age(in_age), height(in_height), money(in_money){std::cout << "有参构造函数" << std::endl;}
};int main(){People p = People(100, 200, 300);std::cout << p.age << ", " << p.height << ", " << p.money << std::endl;return 0;
}
4.3.8 类对象作为类成员
C++中类的成员可以是另一个类的对象,我们成为对象成员。
Example:
#include "iostream"
using namespace std;class Phone{
public:string name;
public:Phone(string in_name):name(in_name){cout << "Phone的有参构造函数" << endl;}~Phone(){cout << "Phone的析构函数" << endl;}
};class People{
public:int age;Phone phone;
public:People(int in_age, string in_phone):age(in_age), phone(in_phone){cout << "People的有参构造函数" << endl;}~People(){cout << "People的析构函数" << endl;}
};int main(){string name = "iPhone 15 pro max";int age = 22;People people = People(age, name);return 0;
}
4.3.9 静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为 静态成员变量 和 静态成员函数
- 静态成员变量
- 所有对象共享一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
#include "iostream"class People{
public://所有对象共享一份数据//类内声明static std::string country;
private://私有静态变量外部无法访问,只能类内访问static std::string sex;
};//类外初始化
std::string People::country = "China";
std::string People::sex = "Male";int main(){People people = People();//访问方式1:通过对象访问std::cout << people.country << std::endl;//访问方式2:通过作用域访问std::cout << People::country << std::endl;return 0;
}
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量(因为静态成员函数在内存中只存在一份,如果一个对象使用静态成员函数对非静态成员变量进行修改时,程序会不知道修改的是哪一个对象的非静态变量)
#include "iostream"class People{
public:static std::string country;int age;
private:static std::string sex;
public:static void test_fun(){std::cout << "类内静态成员函数" << std::endl;sex = "Female";}
};std::string People::country = "China";
std::string People::sex = "Male";int main(){People people = People();//访问方式1,通过对象people.test_fun();//访问方式2,通过作用域People::test_fun();return 0;
}
4.4 C++对象模型和this指针
4.4.1 成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
总结:类中的四个部分:成员变量、成员函数、静态成员变量、静态成员函数,只有成员变量存储在类中,其它三个部分都存储在类外。
#include "iostream"class People{
public:int age;static int height;
public:void fun1(){}static void fun2(){}
};int main(){People people;std::cout << "size of the class is :" << sizeof(people) << std::endl;return 0;
}
另外,一个空对象的sizeof是1而不是0,这是因为系统在底层为了区分每一个空对象,为每一个空对象分配了一个地址
4.4.2 this指针概念
现在已经知道C++中成员变量和成员函数是分开存储的,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类的对象会共用一块代码,那么问题是:这一块代码是如何区分哪个对象调用自己呢?
C++通过提供特殊的对象指针,this指针,解决上述问题。**this指针指向被调用的成员函数所属的对象(谁调用成员函数this指针就指向谁) **
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接可以使用
this指针的用途:
- 当形参和成员变量同名时,可以用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
#include "iostream"class People{
public:int age;
public:People(int age){this->age = age;}//返回对象引用,返回的是该对象本身People& addPeopleAgeFun1(int age){this->age += age;return *this;}//返回对象,返回的是该对象的拷贝,不是该对象People addPeopleAgeFun2(int age){this->age += age;return *this;}
};int main(){People people = People(10);std::cout << "the age of the people is: " << people.age << std::endl;people.addPeopleAgeFun1(20).addPeopleAgeFun1(20);std::cout << "the age of the people is: " << people.age << std::endl;people.addPeopleAgeFun2(20).addPeopleAgeFun2(20);std::cout << "the age of the people is: " << people.age << std::endl;return 0;
}
4.4.3 空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是要注意有没有用到this指针,如果用到this指针,需要加以判断保证代码的健壮性。
#include "iostream"class People{
public:int age;
public:void showClassName(){std::cout << "this is a People class" << std::endl;}void showAge(){//age默认加了this->age//为了保证健壮性,可以现在函数前边写上//if (!this){// return;//}std::cout << "age is:" << age << std::endl;}
};int main(){People* people = NULL;//正常调用people->showClassName();//出现错误//people->showAge();return 0;
}
4.4.4 const修饰成员函数
常函数:
- 成员函数后加const后我们称这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
#include "iostream"class People{
private:mutable int age;
public:// 常函数不能修改类属性,除非属性前边加了mutablevoid showAge() const{this->age = 10;std::cout << this->age << std::endl;}void fun(){}
};int main(){People people = People();people.showAge();const People constPeople = People();constPeople.showAge();return 0;
}
另外:this指针本质上是一个指针常量,指针指向不可以修改
4.5 友元
生活中家里有客厅(Public),有卧室(Private)
客厅所有来的客人都可以进去,但是卧室是私有的,也就是说只有自己能进去
但是呢,你也可以允许你的好闺蜜好基友进去
在程序里,有些 私有属性 也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术
友元的目的就是让一个 函数或者类 访问另一个类中私有成员
友元的关键字为friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
4.5.1 全局函数做友元
#include "iostream"class Building{friend void showPrivateProperties(Building &building);
private:std::string sittingRoom;std::string bedroom;
public:Building(){this->sittingRoom = "客厅";this->bedroom = "卧室";}
};void showPrivateProperties(Building &building){std::cout << building.sittingRoom << std::endl;std::cout << building.bedroom << std::endl;
}int main(){Building building = Building();showPrivateProperties(building);return 0;
}
4.5.2 类做友元
#include "iostream"class Building;
class Visitor;class Building{friend class Visitor;
private:std::string bedroom;
public:std::string sittingRoom;
public:Building();
};class Visitor{
private:Building* building;
public:Visitor();void visitBuilding();
};//类外实现构造函数
Building::Building() {this->sittingRoom = "客厅";this->bedroom = "卧室";
}
Visitor::Visitor() {this->building = new Building;
}
void Visitor::visitBuilding() {std::cout << this->building->sittingRoom << std::endl;std::cout << this->building->bedroom << std::endl;
}int main(){Building building = Building();Visitor visitor = Visitor();visitor.visitBuilding();return 0;
}
4.5.3 成员函数做友元
#include "iostream"class Building;
class Visitor;class Visitor{
private:Building* building;
public:Visitor();void visitBuilding();
};class Building{friend void Visitor::visitBuilding();
private:std::string bedroom;
public:std::string sittingRoom;
public:Building();
};//类外实现构造函数
Building::Building() {this->sittingRoom = "客厅";this->bedroom = "卧室";
}
Visitor::Visitor() {this->building = new Building;
}
void Visitor::visitBuilding() {std::cout << this->building->sittingRoom << std::endl;std::cout << this->building->bedroom << std::endl;
}int main(){Building building = Building();Visitor visitor = Visitor();visitor.visitBuilding();return 0;
}
4.6 运算符重载
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
4.6.1 加号运算符重载
作用:实现两个自定义数据类型相加的运算
#include "iostream"
using namespace std;class People{
public:int age;// 重载加号方式1:成员函数重载People operator+(const People &p) {People temp;temp.age = this->age + p.age;return temp;}People operator+(int a){People temp;temp.age = this->age + a;return temp;}
};// 重载加号方式2:全局函数重载
People operator+(const People &p1, const People &p2) {People temp;temp.age = p1.age + p2.age;return temp;
}People operator+(const People &p, int a){People temp;temp.age = p.age + a;return temp;
}int main(){People p1 = People(18);People p2 = People(19);// 重载加号方式1:成员函数重载,本质上是 p3 = p1.operator+(p2)// 重载加号方式2:全局函数重载,本质上是 p3 = operator+(p1, p2)// 如果两种重载方式都写了,优先调用成员函数重载People p3 = p1 + p2;cout << p3.age << endl;// 重载运算符也可以发生函数重载People p4 = p1 + 2;cout << p4.age << endl;return 0;
}
总结1:对于内置数据类型的表达式的运算符是不可能发生改变的
总结2:不要滥用运算符重载
4.6.2 左移运算符重载
作用:可以输出自定义数据类型
#include "iostream"using namespace std;class People{friend ostream& operator<<(ostream &cout, People &people);
private:int age;string name;
public:People(int age, string name) {this->age = age;this->name = name;}
};// 左移运算符只能通过全局函数做重载
ostream& operator<<(ostream &cout, People &people){cout << "people->age:" << people.age << ", people->name:" << people.name;return cout;
}int main(){People people = People(23, "AirCL");cout << people << endl;return 0;
}
4.6.3 递增运算符重载
以下代码同样可以使用在重载递减运算符中(--)
#include "iostream"
using namespace std;class MyInteger{friend ostream& operator<<(ostream& cout, const MyInteger& myInteger);
private:int value;
public:MyInteger(){this->value = 0;}//重载前置++运算符//重载前置++时,返回引用类型,为了能够实现连续自增MyInteger& operator++(){this->value++;return *this;}//重载后置++运算符//重载后置++时,返回对象实体,而不返回引用MyInteger operator++(int){//1.记录当前值MyInteger temp = *this;//2.递增当前值this->value++;//3.返回之前记录的值return temp;}
};// 重载 << 运算符
ostream& operator<<(ostream& cout, const MyInteger& myInteger){cout << myInteger.value;return cout;
}int main(){MyInteger num;//模拟前置递增cout << num << endl;cout << ++(++num) << endl;cout << num << endl;//模拟后置递增cout << num << endl;cout << num++ << endl;cout << num << endl;return 0;
}
总结:前置递增返回引用,后置递增返回实体。
4.6.4 赋值运算符重载
C++编译器至少给一个类添加4个函数
-
默认构造函数(无参,函数体为空)
-
默认析构函数(无参,函数体为空)
-
默认拷贝构造函数,对属性进行值拷贝
-
赋值运算符 operator=,对属性进行值拷贝
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题
#include "iostream"
using namespace std;class People{friend ostream& operator<<(ostream& cout, const People& people);;
private:int *age;
public:People(int age){this->age = new int(age);}~People(){if(this->age){delete this->age;this->age = NULL;}}//重载 =People& operator=(People &people){//如果直接使用this->age = people.age;//这样的浅拷贝在两个对象销毁调用析构函数时会因为指针指向同一块内存而出现问题//因此应该使用深拷贝,如下this->age = new int(*people.age);return *this;}
};ostream& operator<<(ostream& cout, const People& people){cout << "People->age: " << *people.age;return cout;
}int main(){People people_1 = People(10);People people_2 = People(20);People people_3 = People(30);people_1 = people_2 = people_3;cout << people_1 << endl;cout << people_2 << endl;cout << people_3 << endl;return 0;
}
4.6.5 关系运算符重载
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作。
#include "iostream"
#include "string"
using namespace std;class People{
public:string name;int age;People(string name, int age){this->name = name;this->age = age;}// 重载 ==bool operator==(People& p){if(this->name == p.name && this->age == p.age){return true;}else{return false;}}// 重载 !=bool operator!=(People& p){if(this->name != p.name || this->age != p.age){return true;} else{return false;}}
};int main(){People p1 = People("people_1", 18);People p2 = People("people_1", 18);if(p1 == p2){cout << "相等" << endl;}else{cout << "不相等" << endl;}if(p1 != p2){cout << "不相等" << endl;}else{cout << "相等" << endl;}return 0;
}
4.6.6 函数调用运算符重载
- 函数调用运算符 () 也可以重载
- 由于重载后使用的方式非常像函数的调用,因此被称为 仿函数
- 仿函数没有固定写法,非常灵活
#include "iostream"
using namespace std;class FunctorPrint{
public:void operator()(string str){cout << str << endl;}
};class FunctorAdd{
public:int operator()(int a, int b){return a + b;}
};int main(){FunctorPrint functorPrint;functorPrint("ありがとうございました");// 匿名对象形式调用仿函数FunctorPrint()("test print");cout << FunctorAdd()(1, 2);return 0;
}
4.7 继承
继承是面向对象三大特性之一
有些类与类之间存在特殊的关系,例如下图中:
我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。
这个时候我们就可以考虑利用继承的技术,减少重复代码。
4.7.1 继承的基本语法
语法:class 子类 : 继承方式 父类
子类也称派生类,父类也称基类。
继承的好处:可以减少重复的代码。
例如我们看到很多网站中,都有公共的头部、公共的底部。甚至公共的左侧列表,只有中心内容不同。
接下来分别使用普通写法和继承写法来实现网页中的内容,看一看继承存在的意义以及好处。
普通写法:
#include "iostream"
using namespace std;/** 普通写法* */
class JavaPage{
public:void header(){cout << "page header" << endl;}void footer(){cout << "page footer" << endl;}void classification(){cout << "Java、Python、C++、..." << endl;}void content(){cout << "Java 学科视频" << endl;}
};class PythonPage{
public:void header(){cout << "page header" << endl;}void footer(){cout << "page footer" << endl;}void classification(){cout << "Java、Python、C++、..." << endl;}void content(){cout << "Python 学科视频" << endl;}
};class CPlusPage{
public:void header(){cout << "page header" << endl;}void footer(){cout << "page footer" << endl;}void classification(){cout << "Java、Python、C++、..." << endl;}void content(){cout << "C++ 学科视频" << endl;}
};int main(){/** 普通写法* */JavaPage javaPage;PythonPage pythonPage;CPlusPage cPlusPage;cout << "==========================================" << endl;javaPage.header();javaPage.classification();javaPage.content();javaPage.footer();cout << "==========================================" << endl;pythonPage.header();pythonPage.classification();pythonPage.content();pythonPage.footer();cout << "==========================================" << endl;cPlusPage.header();cPlusPage.classification();cPlusPage.content();cPlusPage.footer();return 0;
}
继承写法:
#include "iostream"
using namespace std;/*** 继承写法*/
class BasePage{
public:void header(){cout << "page header" << endl;}void footer(){cout << "page footer" << endl;}void classification(){cout << "Java、Python、C++、..." << endl;}
};
class JavaPage:public BasePage{
public:void content(){cout << "Java 学科视频" << endl;}
};
class PythonPage:public BasePage{
public:void content(){cout << "Python 学科视频" << endl;}
};
class CPlusPage:public BasePage{
public:void content(){cout << "C++ 学科视频" << endl;}
};int main(){/** 普通写法* */JavaPage javaPage;PythonPage pythonPage;CPlusPage cPlusPage;cout << "==========================================" << endl;javaPage.header();javaPage.classification();javaPage.content();javaPage.footer();cout << "==========================================" << endl;pythonPage.header();pythonPage.classification();pythonPage.content();pythonPage.footer();cout << "==========================================" << endl;cPlusPage.header();cPlusPage.classification();cPlusPage.content();cPlusPage.footer();return 0;
}
4.7.2 继承方式
继承的语法:
class 子类 : 继承方式 父类
继承方式一共有三种:
- 公共继承
- 保护继承
- 私有继承
4.7.3 继承中的对象模型
问题:从父类继承过来的成员,哪些属于子类对象中?
class Base{
public:int a;
protected:int b;
private:int c;
}class Son : public Base{
public:int d;
}int main(){Son son;cout << sizeof(son) << endl;// 结果为16// 父类中所有的非静态成员属性都会被子类继承下去// 父类中私有成员属性是被编译器隐藏了,因此是访问不到,但是的确被继承了return 0;
}
结论:父类中所有的非静态成员属性都会被子类继承下去,父类中私有成员属性是被编译器隐藏了,因此是访问不到,但是的确被继承了
4.7.4 继承中构造和析构顺序
子类继承父类后,当创建子类对象时,也会调用父类的构造函数。
问题:父类和子类的构造函数和析构函数顺序是谁先谁后?
结论:和自然界顺序一样,先有父类再有子类,顺序也就是:父类构造、子类构造、子类析构、父类析构
4.7.5 继承同名成员处理方式
问题:当子类和父类出现同名的成员时,如何通过子类对象,访问到子类或者父类中同名的数据呢?
结论:
- 访问子类同名成员,直接访问
- 访问父类同名成员,需要加作用域
class Base{
public:int a;void func(){cout << "Base Func" << endl;}//另外,当父类中出现对同名成员函数的重载时//对其进行调用时,必须加上作用域,s.Base::func(10)/**原理:如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有的同名成员函数,如果想访问到父类中被隐藏的同名成员函数,需要加作用域*/void func(int k){cout << "Base Overload Func:" << k << endl;}
}class Son : public Base{
public:int a;void func(){cout << "Son Func" << endl;}
}int main(){Son son;//访问子类成员cout << s.a << endl;//访问父类成员cout << s.Base::a << endl;//访问子类成员cout << s.func() << endl;//访问父类成员cout << s.Base::func() << endl;return 0;
}
总结:
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
4.7.6 继承同名静态成员处理方式
问题:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致,只不过有两种调用方式:
- 子类对象可以直接访问到子类中同名静态成员
- 子类对象加作用域可以访问父类同名静态成员
class Base{
public:// 静态成员属性,类内声明,类外初始化static int a;
}
int Base::a = 10;class Son : public Base{
public:static int a;
}
int Son::a = 20;int main(){//两种方式访问静态成员属性,成员函数也一样//1.通过对象访问cout << s.a << endl;cout << s.Base::a << endl;//2.通过类名访问cout << Son::a << endl;cout << Son::Base::a << endl;return 0;
}
4.7.7 多继承语法
C++允许一个类继承多个类
语法:
class 子类 : 继承方式 父类1, 继承方式 父类2...{
}
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议用多继承
4.7.8 菱形继承
菱形继承概念:两个派生类继承同一个基类,又有某个类同时继承两个派生类,这种继承方式成为菱形继承,或者钻石继承。
典型案例:
菱形继承的问题:当(羊驼)继承(羊)和(驼),且(动物)中有属性给子类继承时,(羊驼)继承的属性会存在“来历不明”的问题,也就是(羊驼)继承的属性不明确来自于(羊)还是(驼)
通过虚继承可以解决菱形继承的问题
class Animal{
public:int age;
}class Sheep : virtual public Animal{}
class Camel : virtual public Animal{}class Alpaca : public Sheep, public Camel{}
内部原理:
4.7 多态
4.8.1 多态的基本概念
多态是C++面向对象三大特性之一
多态分为两类
- 静态多态:函数重载 和 运算符重载 属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定,编译阶段确定函数地址
- 动态多态的函数地址晚绑定,运行阶段确定函数地址
class Animal{
public:// 此处若不加 virtual ,则为静态多态,此函数的地址在编译阶段即绑定,main中输出为 "the spaek of animals"void speak(){cout << "the spaek of animals" << endl;}// 此处加 virtual ,则为动态多态,此函数的地址在运行阶段绑定,main中输出为 "the spaek of cats"virtual void speak(){cout << "the spaek of animals" << endl;}
};class Cat{
public:void speak(){cout << "the spaek of cats" << endl;}
};void speak(Animal& animal){animal.speak();
}int main(){Cat cat;// C++默认会对父子类进行强制类型转换speak(cat);return 0;
}
总结:
动态多态满足条件:
- 有继承关系
- 子类重写父类的虚函数
动态多态的使用:
- 父类的指针或引用指向子类对象
重写于重载不同,重写是函数的返回值类型、函数名、参数列表完全一致
4.8.2 多态原理
一个接口,多种方法
-
用virtual关键字声明的函数叫做虚函数,虚函数肯定是类的成员函数。
-
存在虚函数的类都有一个虚函数表叫做虚表。当类中声明虚函数时,编译器会在类中生成一个虚函数表。
-
类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
-
虚函数表是一个存储类成员函数指针的数据结构。
-
虚函数表是由编译器自动生成与维护的。
-
virtual成员函数会被编译器放入虚函数表中。
-
当存在虚函数时,每个对象中都有一个指向虚函数的指针(C++编译器给父类对象,子类对象提前布局vptr指针),当进行test(parent *base)函数的时候,C++编译器不需要区分子类或者父类对象,只需要在base指针中,找到vptr指针即可)。
-
vptr一般作为类对象的第一个成员。
虚表:虚表是通过一块连续的内存来存储虚函数的地址。这张表解决了继承、虚函数(重写)的问题。在有虚函数的对象实例中都存在这样虚表指针,指向虚函数表,虚表它就像一张地图,指向了实际调用的虚函数。
4.8.3 多态案例——计算器实现
多态的好处:
- 组织结构清晰
- 可读性强
- 前后期扩展和可维护性高
普通方法实现计算器
class Calculator{
public:int a;int b;int getResult(string oper){if (oper == "+"){return this->a + this->b;}else if(oper == "-"){return this->a - this->b;}else if(oper == "*"){return this->b * this->b;}// 如果想扩展除法,必须修改此类,添加上除法else if(oper == "/"){return this->a / this->b;}}
};
多态实现计算器
class Calculator{
public:int a;int b;virtual int getResult(){return 0;}
}class AddCalculator : public Calculator{
public:int getResult(){return this->a + this->b;}
}class SubCalculator : public Calculator{
public:int getResult(){return this->a - this->b;}
}class MulCalculator : public Calculator{
public:int getResult(){return this->a * this->b;}
}// 使用多态写法,只需要添加新的类,并在新的类中重写父类的接口即可
class DivCalculator : public Calculator{
public:int getResult(){return this->a / this->b;}
}int main(){//父类的指针或者引用指向子类的对象Calculator* cal = new AddCalculator;cal->a = 10;cal->b = 20;cout << "add function:" << cal.getResult() << endl;delete cal;return 0;
}
总结:C++开发提倡利用多态设计程序架构,因为多态优点很多
4.8.4 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:
virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
4.8.5 虚析构和纯虚析构
多态使用时,父类指针在释放时无法调用到子类的析构代码,如果子类中有属性开辟到堆区,就会造成内存泄露
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构:
class Animal{
public:virtual void speak() = 0;virtual ~Animal(){//若父类中有堆区内容,也可在此销毁cout << "Animal 析构" << endl;}
}class Cat : public Animal{
public:string* name;Cat(string name){this->name = new string(name);}void speak(){cout << this->name << "is speaking" << endl;}~Cat(){if (!this->name){delete this->name;this->name = NULL;}cout << "Cat 析构" << endl;}
}
纯虚析构:
class Animal{
public:virtual void speak() = 0;virtual ~Animal() = 0;
}// 这一块代码当需要对父类中堆区内容进行销毁时才写
Animal::~Animal(){//销毁堆区内容
}class Cat : public Animal{
public:string* name;Cat(string name){this->name = new string(name);}void speak(){cout << this->name << "is speaking" << endl;}~Cat(){if (!this->name){delete this->name;this->name = NULL;}cout << "Cat 析构" << endl;}
}
总结:
- 虚析构和纯虚析构都是用来解决通过父类指针指向子类对象无法释放子类堆区属性的问题
- 如果子类中没有堆区数据,可以不写虚析构和纯虚析构
- 拥有纯虚析构函数的类也属于抽象类
5. 文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束就会被释放
通过文件可以将数据持久化
C++中对文件操作需要包含头文件 < fstream >
文件类型分为两种:
- 文本文件 - 文件以文本的ASCII码形式存储在计算机中
- 二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
- ofstream : 写操作
- ifstream : 读操作
- fstream : 读写操作
5.1 文本文件
5.1.1 写文件
写文件步骤如下:
- 包含头文件
#include<fstream>
- 创建流对象
ofstream ofs;
- 打开文件
ofs.open("文件路径", 打开方式);
- 写数据
ofs << "写入的数据";
- 关闭文件
ofs.close()
文件打开方式:
打开方式 | 解释 |
---|---|
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在先删除,再创建 |
ios::binary | 二进制方式 |
注意:文件打开方式可以配合使用,利用 | 操作符
例如:用二进制方式写文件
ios::binary | ios::out
实例:
#include <iostream>
#include <fstream>
using namespace std;int main(){ofstream ofs;//这个Clion用相对路径写不进去文件,不知道为什么string filePath = "C:\\AirCL_Main\\CLionWorks\\CPlusStudy\\core\\files\\testFiles\\test.txt";ofs.open(filePath, ios::out);ofs << "测试文件写入" << endl;ofs << "虽然不知道往里边写什么" << endl;ofs.close();return 0;
}
总结:
- 文件操作必须包含头文件 fstream
- 读文件可以利用 ofstream ,或者fstream类
- 打开文件时候需要指定操作文件的路径,以及打开方式
- 利用 << 可以向文件中写数据
- 操作完毕,要关闭文件
5.1.2 读文件
- 包含头文件
#include<fstream>
- 创建流对象
ifstream ifs;
- 打开文件
ifs.open("文件路径", 打开方式);
- 读数据
四种读取方式
- 关闭文件
iofs.close()
实例:
#include <iostream>
#include <fstream>
using namespace std;int main(){string file_path = "C:\\AirCL_Main\\CLionWorks\\CPlusStudy\\core\\files\\testFiles\\test.txt";ifstream ifs;ifs.open(file_path, ios::in);if(!ifs.is_open()){cout << "文件打开失败!" << endl;}/*** 四种读文件方式*///第一种
// char buff[1024] = {0};
// while (ifs >> buff){
// cout << buff << endl;
// }//第二种
// char buff[1024] = {0};
// while (ifs.getline(buff, sizeof(buff))){
// cout << buff << endl;
// }//第三种
// string buff;
// while (getline(ifs, buff)){
// cout << buff << endl;
// }//第四种//第四种不要加endl,文件中的换行能够被读到,并且加上endl后会分割字符导致读取内容全是空格
// char c;
// // EOF = End Of the File
// while ((c = ifs.get()) != EOF){
// cout << c;
// }ifs.close();return 0;
}
5.2 二进制文件
以二进制的方式对文件进行读写操作
打开方式要指定为 ios::binary
5.2.1 写文件
二进制方式写文件主要利用流对象调用成员函数write
函数原型:
ostream& write(const char * buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
#include <iostream>
#include <fstream>
using namespace std;class People{
public:char name[10];int age;
};int main(){string filePath = "C:\\AirCL_Main\\CLionWorks\\CPlusStudy\\core\\files\\testFiles\\testBinary.txt";ofstream ofs(filePath, ios::out | ios::binary);People people = {"张三", 20};ofs.write((const char *)&people, sizeof(people));ofs.close();return 0;
}
总结:二进制形式写文件重点在于将数据的地址转成const char *通过write函数以数据大小写入
5.2.2 读文件
二进制方式读文件主要利用流对象调用成员函数read
函数原型:
ifstream& read(char* buffer, int len)
参数解释:字符指针buffer指向内存中一段存储空间,len是读取的字节数
#include <iostream>
#include <fstream>
using namespace std;class People{
public:char name[10];int age;
};int main(){string filePath = "C:\\AirCL_Main\\CLionWorks\\CPlusStudy\\core\\files\\testFiles\\testBinary.txt";ifstream ifs(filePath, ios::in | ios::binary);if (!ifs.is_open()){cout << "文件打开失败!" << endl;}People people;ifs.read((char *)&people, sizeof(people));cout << people.name << endl;cout << people.age << endl;ifs.close();return 0;
}
总结:二进制方式读取数据的重点是将数据将要写入的内存取地址强转为char *,再通过read()函数以内存的大小读入