内存模型
C++在执行程序的时候,将内存方向划分为4个区域:
-
代码区:存放二进制代码,由操作系统进行管理
-
全局区:存放全局变量、静态变量、常量,程序结束后由操作系统释放
-
栈区:存放函数参数、局部变量,由编译器自动分配和释放
-
堆区:由开发者申请分配和释放,若程序员不释放,程序结束由操作系统自动回收
意义:对于不同区域存放的数据,赋予不同的生命周期,给编程更大的灵活性。
代码区
存放CPU执行的二进制代码(机器指令)
特点:
-
共享:对于频繁被执行的程序,只需要在内存中有一份就够了
-
只读:防止被意外修改
全局区
-
存放全局变量和静态变量,还存放常量,包括字符串常量和其他常量
-
数据在程序结束后由操作系统进行释放
栈区
-
存放函数参数、局部变量
-
不要返回局部变量的地址,因为函数一执行完,栈区数据就被释放了,虽然编译器会做短暂的保留
堆区
-
这是由开发者分配和释放的,如果程序结束开发者不释放,也会操作系统回收
-
在C++中主要用new开辟堆区空间,用delete释放
new 和 delete
new作用:用于让开发者在堆区中开辟数据
delete作用:让开发者手动释放堆区数据
语法:
new 数据类型
delete 堆区地址
示例:
int *p = new int(8); //在堆区开辟一个int类型的内存,存放数据8
int *p = new int[10]; //在堆区开辟一个int类型的内存存放数组,数组中有10个元素
delete(p); //释放地址a的数据
引用
作用:给变量起别名
语法:
数据类型 &别名 = 原名
注意事项:
-
引用必须初始化
-
初始化后,就不可以再发生改变了
-
引用必须引一块合法的内存空间,可以是栈区,可以是堆区,但不可以是自变量(比如数字)
示例:
int a = 10;
int &b = a; //引用,且必须初始化
int &b = 10; //×,错误,引用必须引一块合法的内存,10是自变量,既不是栈区也不是堆区
const int &b = 10; //√,正确,加上const后,编译器会开辟出一块临时内存,int temp = 10,const int &b = temp;
引用做函数参数
作用:可以让形参修饰实参,代替指针中形参修改实参的操作
示例:
void myswap(int &x, int &y) //引用就是取别名,所以参数就是实参,所以可以改变实参
{
int temp = x;
x = y;
y = temp;
}
int main
{
int a = 10;
int b = 20;
myswap(a , b); //引用传递,形参可以修改实参
system("pause");
}
引用做函数返回值
作用:可以作为函数返回值类型返回
注意事项:不要返回局部变量的引用,函数执行完局部变量内存就被释放了,返回个锤子
示例:
int& test() //函数返回值类型就是引用类型
{
static int a = 10; //加个关键字static,这样变量a就不是关键字了
return a;
}
int main()
{
int &ref = test();
}
引用的本质
本质:引用的本质在C++内部实现,它就是一个指针常量,由编译器内部转换
作用:也就说明为什么引用初始化之后就不可更改,因为指针指向不可改
& <—等于—> int* const
示例:
int& ref = a; <==> int* const ref = &a
ref = 20; <==> *ref = 20
常量引用
作用:主要用来修饰形参,防止误操作
用法:在函数形参列表中,加const修饰形参,防止形参改变实参
示例:
void temp(const int& val)
{
}
函数进阶用法
函数的默认参数
在C++中,函数的形参列表中形参是可以用默认参数的
语法:
返回值类型 函数名 (参数 = 默认值)
{
}
注意事项:
-
如果函数某个参数有默认值,那么从这个位置之后的参数必须有默认值
-
如果调用的时候有实参,那就用实参,没有实参,就用默认值
-
如果函数声明有默认值,那么在函数定义的时候就不能有默认值
示例:
int func1(int a, int b=10, int c=20) //往后如果还有参数,必须要有默认值
int func2(int a=10, int b=20) //函数声明有默认值了
int func2(int a, int b) //函数实现就不能有默认值了
{
}
函数的占位参数
作用:用来给函数的参数列表中做占位,调用函数的时候填补该位置就行了
语法:
返回值类型 函数名(数据类型)
{
}
缺点:现阶段函数的占位函数存在意义不大。
示例:
void func(int a, int) //int 就是占位参数了,只需要写一个数据类型即可
{
}
int main
{
func(10,20); //调用的时候占位函数要补上
}
函数重载
作用:函数名相同,其他的可以不同,可以提高函数的复用性
满足条件:
-
同一个作用域下
-
函数名称相同
-
函数参数类型不同,或者个数不同,或者顺序不同
注意事项:
-
函数的返回值不能作为函数重载的满足条件
-
具体调用的是哪一个函数,就看参数,看实参是否对应形参,比如类型、个数、顺序
示例:
void func() //func是函数重载,这是在全局作用域下
{
}
void func(int a) //参数类型不同,这是在全局作用域下
{
}
void func(double a,double b) //参数个数不同,这是在全局作用域下
{
}
void func(double a, int b) //参数顺序不同,这是在全局作用域下
{
}
引用作为函数重载
当引用作为函数参数时:
-
实参必须是一块合法的内存
-
如果实参不是内存,只是一个自变量,那么形参就必须加const来修饰
示例:
void func(int &a)
{
}
void func(const int &a) //这两个func是函数重载
{
}
int main()
{
int a = 10;
func(a); //调用的是第一个func函数,因为a是变量,是一块合法内存
func(10); //调用的是第二个func函数,因为10是自变量,加const修饰本质上是申请一块临时内存存放数据10
}
遇到默认参数
-
当函数重载遇到默认参数时,会出现二义性,也就是出错
-
使用时尽量避免出现默认参数
示例:
void func(int a)
{
}
void func(int a, int b = 10)
{
}
int main()
{
func(10); //❌,编译器懵了,不知道该调用哪一个func
}
类与对象
-
C++本身就是面向对象的编程语言
-
面向对象三大特性:封装、继承、多态
-
C++中万物皆可为对象,对象上有属性和行为
-
具有相同性质的对象,称之为类
示例:
人可以作为对象,属性有姓名、年龄、身高···,行为有唱,跳、rap···
你和你的死党,是同一性质,属于人类;
车可以作为对象,属性有轮胎、车灯、方向盘···,行为有载人、音乐、显摆···
五菱与奥迪,是同一性质,属于车类;
封装
封装的意义
封装是C++面向对象三大特性之一
封装的意义:
-
将属性和行为作为一个整体,来表现生活中的事物
-
将属性和行为用权限加以控制
封装的术语:
-
类中的属性和行为,统称为成员
-
属性也叫成员属性或者成员变量
-
行为也叫成员函数或者成员方法
封装意义一:将属性和行为作为一个整体
语法:
class 类名{ 访问权限:属性/行为 }
示例:创建一个类为圆,那半径就是它的属性了。
class Circle
{
public: //设置访问权限,公共权限
double m_r; //属性——半径,
double calculate() //行为——计算周长
{
return 2 * PI * m_r;
}
}
int main()
{
Circle C1; //通过一个类创建一个对象,对象就是圆,也就是实例化
C1.m_r = 10;
cout << "圆的周长:" << C1.calculate() << endl;
}
封装意义二:将属性和行为用访问权限来加以管理
访问权限有三种:
-
public:公共权限,成员类内和类外都可以访问
-
protected:保护权限,成员类内可以访问,,类外不可以访问
-
private:私有权限,成员类内可以访问,类外不可以访问
protecred保护权限和private私有权限的区别在于后面要说到的继承上,前者子类可以访问父类,后者子类不可访问父类,这是后面的内容了。
示例:
class person
{
public:
string m_name; //姓名,类内类外都可以访问
protected:
string m_car; //汽车,类内可以访问,类外不可以,家里的汽车只有家里人能用,外人不可以
private:
int m_password; //银行卡密码,类内极度私密,只有当事人能用,其他任何人甚至儿子也不能用
public:
void func()
{
m_name = "张三"; //类内可以访问
m_car = "大众"; //类内可以访问
m_password = 123456; //类内可以访问
}
}
int main()
{
person c1;
c1.m_name = "李四"; //√,类外可以访问
c1.m_car = "吉利"; //×,类外不可以访问
c1.m_password = 456789; //×,类外不可以访问
c1.func(); //√,类外是可以访问的
}
struct和class
struct和class都可以表示一个类,区别在于两者默认的权限不同:
-
struct:默认权限为公共
-
class:默认权限为私有
成员属性设置为私有
优点:
-
所有成员设置成私有,自己可以控制读写权限
-
对于写权限,可以检查其数据的有效性
示例:所有成员设置成私有,自己可以控制读写权限
class person
{
private:
string m_name; //成员设置成私有权限,一般通过成员函数进行访问
int m_age;
string m_lover;
public:
void SetName(string name) //这样name就被设置成了可读可写的
{
m_name = name;
}
int getage() //age就被设置成了只读
{
m_age = 18;
return m_age;
}
}
int main()
{
person c1;
c1.m_name = "张三"; //×,因为成员是私有权限,没法访问
c1.SetName("张三"); //√,设置姓名
cout << c1.getage() << endl; //√,获取年龄
}
示例:对于写权限,可以检查其数据的有效性
class person
{
private:
string m_name;
int age;
public:
int GetAge(int age) //获取年龄,设置成可读可写,如果想修改年龄,范围必须是0—100
{
if( age < 0 || age >100 ) //加了判断,可以判断数据是否有效
{
cout << "年龄错了"<< endl;
return ;
}
m_age = 0;
}
}
对象的初始化和清理
C++中,初始化和清理是非常重要的安全问题:
-
构造函数:在创建对象时为对象成员属性初始化,该函数由编译器自动调用
-
析构函数:在对象销毁前系统自动调用,执行一些清理工作
完成对象的初始化和清理工作是编译器必须要我们做的事情,这两个函数由编译器自动调用,但是如果我们不写构造函数和析构函数,编译器就会自己去实现,只不过函数里面是空的,简称空实现
构造函数
语法:
类名(){}
特点:
-
不用写void,没有返回值
-
函数名与类名相同
-
可以有参数,因为可以发生函数重载
-
在调用对象时编译器自动调用,而且只会调用一次
析构函数
语法:
~类名(){}
特点:
-
不用写void,没有返回值
-
函数名与类名相同,且在前面加~
-
不可以有参数,因此不可以发生函数重载
-
在对象销毁前编译器自动调用,而且只调用一次
示例:
class Person
{
public:
Person() //这就是构造函数,如果不写,编译器会自己写一个
{
}
~Person() //这就是析构函数,如果不写,编译器会自己写一个
{
}
}
构造函数的分类和调用
两种分类方式:
-
按参数分:有参构造和无参构造(默认构造)
-
按类型分:普通构造和拷贝构造
三种调用方式:
-
括号法
-
显示法
-
隐式转换法
示例:
class Peoson()
{
public:
Person() //无参构造,也就是普通构造
{
}
Person(int a) //有参构造
{
}
Person(const Person &p) //拷贝构造
{
}
}
int main()
{
//1、括号法调用
Person P1; //普通调用,不用加(),不然编译器会误以为是函数声明
Person P2(10); //调用的是对应的有参构造
Person P3(P1); //调用的是对用的拷贝构造
//2、显示法调用
Person P1;
Person P2 = Person(10); //调用的是对应的有参构造
Person P3 = Person(P2); //调用的是对应的拷贝构造
//3、隐式转换法
Person P1 = 10; //调用的是有参构造,相当于Person P1 = Person(10)
Person P2 = P1; //调用的是拷贝构造,相当于Person P2 = Person(P1)
}
说说拷贝构造函数
所谓拷贝构造函数就是将一个创建完毕对象的属性默认赋值给新的对象,这个赋值过程由编译器自动完成
通常以下三种情况会调用到拷贝构造:
-
使用一个已经创建完毕的对象来初始化一个新对象
-
值传递的方式给函数参数传参
-
以值传递的方式返回局部对象
示例:
class Person()
{
Person()
{
}
Person(const Person &p)
{
}
}
void func1(Person p)
{
}
Person func2()
{
Person a;
return a;
}
int main()
{
Person P1;
Person P2(P1); //使用一个已经创建好的对象来初始化一个新对象
Person P3;
func1(P3); //以值传递的方式给函数传参
Person P4 = func2(); //以值传递的方式返回对象,它会创建一个新的对象来接住返回对象
}
构造函数的调用规则
(1)默认情况下,创建一个类编译器至少会添加3个函数
-
默认构造函数(无参,函数体为空)
-
默认析构函数(无参,函数体为空)
-
默认拷贝函数,对属性进行默认拷贝
(2)如果开发者写了有参构造函数,编译器不再默认提供无参构造,但是会提供默认拷贝构造
(3)如果开发者写了拷贝构造函数,编译器不再提供其他构造函数,无参和有参都没有
浅拷贝和深拷贝
这是经典面试题经常出现的案例,是一个常见的坑
浅拷贝:就是简单的赋值拷贝
深拷贝:在堆区重新开辟一片内存,进行拷贝操作
注意事项:
-
如果涉及到空间开辟和释放,浅拷贝容易发生内存重复释放的非法操作
-
需要自己写一个拷贝构造函数,利用深拷贝去解决浅拷贝带来的内存释放问题
class person
{
public:
person(){}
person(int age,int height)
{
m_age = age; //这就是浅拷贝,就是简单的赋值操作
m_height = new int(height); //这就是深拷贝,在堆区开辟一个新的内存
}
person(const person &p)
{
m_age = p.m_age;
//m_height = p.m_height //这是编译器默认实现的,但是我们不要这样的浅拷贝,会崩
m_height = new int(*p.m_height); //深拷贝,这样不同的对象就有不同的堆区地址,释放就不会发生重复了
}
~person()
{
if(m_height != NULL)
{
delete m_height; //在构造函数手动开辟了堆区,就需要手动释放
m_height NULL;
}
}
private:
int m_age;
int *height;
}
int main()
{
person P1(18,160);
person P2(P1); //将对象P1的属性通过浅拷贝(拷贝构造函数)copy了一份赋值给对象P2了
//同时浅拷贝的还包括有参构造中开辟出来的堆区地址
//这时候如果没有自己写一个拷贝构造函数的话,P1和P2就拥有一样的堆地址
//结束的时候P1和P2释放就释放了相同的堆地址,重复释放属于非法操作,程序会崩
//所以需要在对象中自己写一个拷贝函数,创建一个堆区,让不同对象有不同的堆地址
//这就是利用深拷贝解决浅拷贝带来的内存释放问题
}
初始化列表
作用:用来初始化属性,一般是在构造函数中初始化
语法:
构造函数():属性(初值),属性(初值),属性(初值) { }
示例:
classs person
{
public:
/*
person(int a,int b,int c) //这是传统赋值方式
{
m_A = a;
m_B = b;
m_C = c;
}
*/
/*
person():m_A(10),m_B(20),m_C(30) //初始化列表的方式赋初值
{ //不过这样还是有点不够灵活
}
*/
person(int a,int b,int c):m_A(a),m_B(b),m_C(c) //初始化列表的方式赋初值
{ //比上一个灵活一点
} //不过跟传统方式相比,好处就是逼格高一点而已
private:
int m_A;
int m_B;
int m_C;
}
int main()
{
// person p1(10,20,30); //传统赋初值的方式
// person P1; //利用初始化列表,创建的同时初始化完成
person P1(30,20,10); //初始化列表赋初值,比上一个灵活一点
//不过感觉跟传统方式差不多吧,也就逼格高一点
}
类对象作为类成员
类中的成员可以是一个其他类的对象,该成员就是对象成员
注意事项:
-
创建此类对象的时候,先构造对象成员,再构造自身
-
先析构自身,再析构对象成员
示例:
class Phone //手机类
{
public:
Phone(string pName):m_phonename(pName)
{
}
private:
string m_phonename;
}
class Person //人类
{
public:
//第二个参数相当于:Phone m_phone = pName(隐式转换法,编译器隐藏转换)
Person(string name,string pName):m_name(name),m_phone(pName)
{
}
private:
string m_name;
Phone m_phone; //先创建了Phone类,再有人类
}
int main()
{
Person p("张三","诺基亚"); //在赋值给了人类的同时,也赋值给了手机类
}
静态成员
在成员变量和成员函数前面加一个关键字:static,就成了静态成员
分类:
静态成员变量:
-
所有对象共享一份同一份数据
-
在编译阶段分配内存
-
类内声明,类外初始化
静态成员函数:
-
所有对象共享同一个函数
-
静态成员函数只能访问静态成员变量
根据上面三个特点,静态成员(变量或者函数)不属于某一个对象,所以:
-
public权限的既可以通过对象进行访问,也可以通过类名进行访问
-
private无论如何类外访问不了
示例:
class Person
{
public:
int A;
static int m_A; //类内声明
static void func1()
{
m_A = 50; //只能访问静态变量
//A = 50; //×,静态成员函数只能访问静态成员变量
//因为静态函数只能有一份,而且它在编译的时候就已经有了
//而非静态变量又只能通过类去访问
//编译的时候还没有创建类呢,编译器懵了,不知道这变量是哪一个类的
}
private:
static int m_B; //类内声明
static void func2()
{
}
}
int Person::m_A = 100; //类外初始化
int Person::m_B = 200; //类外初始化
int main()
{
Person P1; //此时m_A就是初始化的100
Person P2;
P2.m_A = 200; //静态m_A已经被改成了200,往后其他类使用m_A数值也是200
//===========================================================//
Person P3;
cout << P3.m_A << endl; //既可以通过对象进行访问
P3.func1();
cout << Person::m_A << endl; //也可以通过类名进行访问
Person::func1();
//cout << P3.m_B << endl; //×,别想了,private权限的静态(变量或者函数)类外访问不了
//cout << Person::m_B << endl; //×
//cout << P3.func2() <<endl; //×
//cout <<Person::fun2()<<endl; //×
}
C++的对象模型
空对象
-
空对象占用1个字节的内存空间
-
每个空对象内存地址独一无二
原因:C++编译器为了区分空对象的所占内存的位置
成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数是分开存储的:
-
非静态成员变量,属于类的对象上的
-
静态成员变量,不属于类的对象上的,不占对象空间
-
成员函数,不属于类的对象上的,只产生一份函数实例,不占对象空间
示例:
class person
{
int m_name; //属于类的对象上,占4字节
static int m_name; //静态成员变量,不属于对象上,不占空间
void func(); //成员函数,不属于对象上,不占空间
}
this指针概念
每一个成员函数只会诞生一份函数实例,说明会有多个对象同时调用同一份函数的情况存在。
用来区分究竟是哪一个对象调用的函数,用的是this指针。
-
this指针是隐藏在每一个成员函数内部的一种特供的指针
-
不需要被定义,本来就是,直接使用即可
-
哪个对象调用了函数,其this指针就指向哪个对象
作用:
-
当形参和成员变量同名时,可用this指针来区分(当然最好还是编程规范)
-
在返回对象本身时,可以用return *this
示例:
class Person
{
Person(int age)
{
//age = age; //×,因为形参和成员变量同名了,编译器会误以为这全是形参,实参就传不进去了
this->age = age; //√,看主函数对象P调用了此构造函数,所以this指针指向P对象
}
int age;
Person& Addage(Person &p) //返回的,就是其本身对象
{
this->age += p.age;
return *this;
}
}
int main()
{
Person P1(18);
Person P2(20);
P2.Addage(P1).Addage(P1).Addage(P1); //√,这就是链式编程思想
//因为P2.Addage(P1)返回的就是对象P2,自然是可以再调用.Addage(),并且可以无限调用下去
cout << "`···"<< P2.age << endl; //其实这个也是链式编程思想
}
空指针访问成员函数
C++中空指针是可以访问成员函数的,主要是要注意有没有用到this指针
-
成员函数中没有用到this指针,空指针可以调用成员函数;
-
成员函数中用到了this指针,空指针就不可以调用成员函数
-
为了保持代码的健壮性,一般会在函数中加入判断 if(this == NULL){ return;}
示例:
Class Person
{
public:
void fun1()
{
cout << "hello" << endl;
}
void fun2()
{
cout << "=" << m_age << endl; //m_age 相当于 this->m_age
}
void fun3()
{
if(this == NULL)
return;
cout << "=" << m_age << endl; //m_age 相当于 this->m_age
}
int m_age;
}
int main()
{
Person *p = NULL; //创建一个空指针
p->fun1(); //√,调用成功,因为函数中没有用到this指针
//p->fun2(); //×,调用失败,因为函数中用到了this指针,就是 m_age 就相当于 this->m_age
p->func3(); //√,虽然里面用到了this指针,但是函数中加入了this指针为空的判断
}
const修饰成员函数
常函数:
-
成员函数后面加const修饰,称之为常函数
-
不可以修改成员属性
-
如果成员属性在声明时加了关键字mutable ,在常函数中就可以修改
常对象:
-
声明对象加const修饰,称之为常对象
-
不可以修改成员属性
-
常对象只能调用常函数,因为普通函数可以修改成员属性
示例:
class Person
{
public:
void func1() const //常函数
{
//this->m_A ; //×,常函数不能修改成员属性
this->m_B; //√,成员属性加了mutable修饰,所以常函数可以修改成员属性
}
void func2()
{
m_A = 100;
}
int m_A;
mutable int m_B;
}
int main()
{
const Person p; //常对象
p.func1(); //对,常对象只能调用常函数
//p.func2(); //×,常对象不可以调用普通函数
}
友元
作用:让一个函数或者类访问另一个类中的私有成员
关键字:friend
3种实现方式:
-
全局函数做友元
-
类做友元
-
成员函数做友元
示例:
class Building
{
friend void Enter(Building *building); //只需要在类最前面加friend声明,全局函数就变成了友元函数
friend class Goodgay; //只需要在类最前面加friend声明,其他的类就变成了本类的友元类
friend void Visit::visit1(); //只需要在类最前面加friend声明,成员函数就变成了本类的友元类,成员函数前需要加上作用域
public:
Buding()
{
this->m_sittingroom = "客厅";
this->m_bedroom = "卧室";
}
public:
string m_sittingroom;
private:
string m_bedroom;
}
void Enter(Building *building)
{
cout << "=" <<building->m_bedroom << endl; //全局函数访问私有成员
}
//========================================================================================================//
class Goodgay
{
Goodgay()
{
building = new Building;
}
void visit0()
{
cout << "=" << building->Bedroom << endl;
}
Building *building;
}
class Visit
{
public:
void visit()
{
building = new Building;
}
void visit1()
{
cout << "=" << building->Bedroom << endl;
}
}
//========================================================================================================//
int main()
{
Building building;
Enter(&building); //全局函数访问私有成员
Goodgay goodgay; //类访问私有成员
googgay.visit0();
Visit visit;
visit.visit1(); //成员函数访问私有成员
}
运算符重载
作用:对已经有的运算符重新进行定义,赋予其另外一种功能,适应不同的自定义数据类型
关键字:operator
-
加号运算符重载
-
左移运算符重载
-
递增运算符重载
-
赋值运算符重载
-
关系运算符重载
-
函数调用运算符重载
运算符重载的方式有2种:
-
成员函数重载
-
全局函数重载
温馨提示:运算符重载也可发生函数重载(函数名相同,函数参数不同)
示例:
Class Person
{
friend ostream& operator<<(ostream &cout, Person &p) //友元函数,不然无法访问私有成员
public;
Person()
{
m_A = 10;
m_B = 20;
}
//成员函数重载:
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;
}
void operator<<(cout) //左移运算符重载
{
//本质就是p.operator<<(cout),也就是 p << cout
//如果想要cout放在左边,成员函数实现不了
//所以一般不用成员函数重载左移运算符,
}
//返回引用是为了一直对同一个数据进行递增操作
Person& operator++() //(前置)递增运算符重载
{
m_A ++;
return *this;
}
//返回值是因为temp只是一个临时变量,函数内用完就被销毁了
Person operator++(int) //(后置)递增运算符重载
{
Person temp = *this;
m_A ++;
return temp;
}
Person& operator=(Person &p) //赋值运算符重载
{
if(m_C != NULL)
{
detele m_C;
m_C = NULL;
}
m_C = new int(* p.m_C);
return *this;
}
bool operator==(Person &p) //关系运算符重载之 == 号(其他的关系运算符写法也是这样的)
{
if(this->m_A== p.m_A && this->m_B == p.m_B)
return ture;
else
return flase;
}
void operator()(string temp) //函数调用运算符重载(写法相当灵活,其对象就是匿名函数对象)
{
cout << temp << endl;
}
int operator()(int temp1, int temp2) //函数调用运算符重载(写法相当灵活,其对象就是匿名函数对象)
{
return temp1+temp2;
}
private:
int m_A;
int m_B;
int *m_C;
}
//全局函数重载:
*/
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;
}
ostream& operator<<(ostream &cout, Person &p) //左移运算符重载
{
//本质就是operator<<(cout,p),也就是 cout << p
//这是访问了私有成员了,该函数需要变成友元函数才行
cout << "m_A = " << p.m_A << "m_B = " << m_B;
return cout;
}
/*
int main()
{
Person p1;
Person p2;
Person p3;
p3 = p1 + p2 ; //加号运算符重载,其实本质就是 p3 = p1.operator+(p2)
cout << p3 ; //左移运算符重载,其实本质就是 operator<<(cout,p3)
cout << ++(++p3) << endl; //前置递增运算符重载
cout << p3++ << endl; //后置递增运算符重载
p1 = p2; //赋值运算符重载,其实本质就是p2.operator=(p2)
if(p1 == p2) //关系运算符之重载 == 号,其本质就是p1.operator==(p2)
cout << "p1 与 p2相等" << endl;
else
cout << "P1 与 p2不相等" << endl;
p1("hello world"); //函数调用运算符,其本质就是p1.operator()(参数)
p1(100, 20); //函数调用运算符,其本质就是p1.operator()(参数)
Person myadd;
Person()(100, 50); //类名()(参数) 这种形式,该对象就是匿名函数对象
//特点: 当前行执行完了,立即被释放
}
继承
-
继承是面向对象三大特性之一
-
有些类与类之前有些特殊的关系,用的就是继承技术,减少重复代码
-
下一级别类除了拥有上一级别类的共性外,还有自己的特性
语法:
class 子类 : 继承方式 父类
子类,也叫派生类;
父类,也叫基类
示例:
class 类名 :public 类名
{
}
继承方式
-
公共继承
-
保护继承
-
私有继承
公共继承:父类私有权限变量不可访问,公共权限和保护权限变量照常继承;
保护继承:父类私有权限变量不可访问,公共权限和保护权限变量变成自己的保护权限变量;
私有权限:父类私有权限变量不可访问,公共权限和保护权限变量变成自己的私有权限变量;
示例:
对象模型
-
父类中所有非静态成员属性都会被子类继承下去
-
父类中的private也会被继承,虽然子类访问不到,那是因为编译器隐藏起来了
-
利用”开发人员命令提示工具“可查看子类继承后的对象模型
工具用法:
-
打开VS软件下的”开发人员命令提示工具“
-
跳转到当前子类所在的文件路径下
-
查看命令:cl /dl reportSingleClassLayout类名 文件名
构造函数和析构函数
子类继承父类中,构造函数和析构函数的顺序:
-
父类 的 构造
-
子类 的 构造
-
子类 的 析构
-
父亲 的 析构
同名成员/函数处理方式
-
访问子类同名成员(变量和函数),直接访问
-
访问父类同名成员(变量和函数),需要加作用域
-
函数重载也一样,就算参数不同也属于同名函数
示例:
Class Base
{
pubcli:
func()
{
}
int m_A;
}
Class Son : public Base
{
public:
func()
{
}
func(int a)
{
}
int m_A;
}
int main()
{
Son son;
son.m_A = 100; //访问的是子类的成员变量
son.Base::m_A = 50; //访问的是父类中的成员变量
son.func(); //访问的是子类中的成员函数
son.Base::func(); //访问的是父类中的成员函数
son.Base::func(50); //函数重载也一样,虽然参数不同,但是父类也需要加作用域
}
静态同名成员
-
与非静态成员处理方式一致(同上)
-
访问子类同名成员,直接访问
-
访问父类同名成员,需要加作用域
多继承
-
C++中允许一个类继承多个父类
-
多继承也会引发父类中同名成员出现,也需要加作用域
-
实际开发中不建议用多继承,容易出现太多二义性
语法:
Class 子类 : 继承方式 父类1 , 继承方式 父类2
菱形继承
-
两个派生类继承同一个基类
-
同时又有某一个类同时继承两个派生类
-
这种继承就被成为菱形继承,或者钻石继承
-
当出现菱形继承时,某一个类就拥有了两份基类的相同数据,需要用作用域加以区分
-
但是我们其实只需要一份数据就够了,多出来的数据纯属就是浪费,用虚继承方式可以解决
-
因为不建议用多继承方式,所以也不建议写菱形继承
-
可以用”开发人员命令提示工具“查看其对象模型
示例:
Class A //父类A
{
public:
int m_age;
}
Class B : public A {}; //派生类B
Class C : public A {}; //派生类C
Class D : public B , public C {}; //子类D,既继承了B,又继承了C,而 B 和 C 又继承于A
int main()
{
Class a;
a.B::m_age = 20; //需要加作用域区分
a.C::m_age = 100;
}
虚继承
关键字:virtual
-
利用虚继承可以解决菱形继承出现的二义性问题
-
在继承之前加上关键词virtual,就变成了虚继承
-
虚继承以最新修改的成员数据为准
-
多份相同数据的来源的那个基类,称之为虚基类
-
因为不建议用多继承方式,所以也不建议写菱形继承
示例:
Class A
{
public:
int m_age;
}
Class B : virtual public A {}; //虚继承
Class C : virtual public A {}; //虚继承
Class D : public B , public C {};
int main()
{
Class a;
a.B::m_age = 20; //有了虚继承,可以加作用域区分,但没必要了
a.C::m_age = 100; //这是最新修改的数据,所有最后数据 m_age 就是 100
cout << a.m_age << endl; //有了虚继承,就不用加作用域区分了,直接访问即可
多态
多态是C++面向对象三大特性之一
多态的分类:
-
静态多态:其函数的地址在编译阶段确定,比如 函数重载 和 运算符重载
-
动态多态:其函数的地址在运行阶段确定,比如 派生类 和 虚函数实现运行
动态多态的满足条件:
-
有继承关系
-
子类重写父类的虚函数
-
用父类的指针或引用来执行子类对象
纯虚函数
-
在多态中,父类中的虚函数通常没有任何意义,因为调用的都是子类重写的函数
-
可以将父类中的虚函数改为 纯虚函数
-
当一个类中有了纯虚函数,这个类被成为抽象类
语法:
virtual 返回值类型 函数名(参数列表)= 0 ;
特点:
-
无法实例化对象
-
子类必须重写抽象类中的纯虚函数,否则也属于抽象类
虚析构和纯虚析构
问题:在使用多态时,如果子类中开辟了堆空间,父类指针在释放时无法调用子类的析构函数
解决办法:将父类中的析构函数改成 虚析构 或者 纯虚析构
二者的共性:
-
可以帮助父类指针释放子类对象
-
都需要有具体的函数实现
-
如果子类中没有开辟堆区,就可以不写虚析构 或者 纯虚析构
-
一个类中如果用于纯虚析构,这个类也被成为抽象类
二者的区别:
-
纯虚析构所在的类属于抽象类,无法实例化对象
-
纯虚析构除了需要声明,也还需要实现
虚析构的语法:
virtual ~类名()
{
}
纯虚析构的语法:
virtual ~类名() = 0 ; //声明
类名::~类名() //实现
{
}
文件操作
-
程序运行时产生的数据都是临时数据,程序一旦执行完毕数据都会被释放
-
C++提供一个文件流的操作,通过文件来让数据持久化
-
文件操作需要的头文件<fstream>
文件类型的分类:
-
文本文件:文件以ASCII的形式存储
-
二进制文件:文件以二进制的形式存
文件操作的分类:
-
ofstream:写操作
-
ifstream:读操作
-
fstream:读写操作
文件打开方式:(多种打开方式用位或操作符 “ | ”隔开即可)
-
ios::in (为读文件而打开)
-
ios::out (为写文件而打开)
-
ios::ate (初始位置:文件尾部)
-
ios::app (以追加方式写文件)
-
ios::trunc (如果文件已经存在,先删除再创建)
-
ios::binary (以二进制方式打开文件)
文本文件
-
写文件
-
包含头文件:#include<fstream>
-
创建文件流对象:ofstream ofs
-
打开文件:ofs.open("文件路径", "打开方式")
-
写数据:ofs << "写入的数据"
-
关闭文件:ofs.close()
示例:
#include<fstream> //1、包含头文件
int main()
{
ofstream ofs; //2、创建流对象
ofs.open("test.txt", ios::out); //3、打开文件
ofs << "姓名:张三" << endl; //4、写数据
ofs << "性别,男" << endl;
ofs << "年龄:18" << endl;
ofs.close(); //5、关闭文件
}
-
读文件
-
包含头文件:#include<fstream>
-
创建文件流对象:ifstream ifs
-
打开文件并判断打开是否成功:ifs.open("文件路径", "打开方式") if( !ofs.is_open() ) { }
-
读数据:有四种读取方式
-
关闭文件:ofs.close()
示例:
#include<fstream> //1、包含头文件
int main()
{
ifstream ifs; //2、创建流对象
ifs.open("test.txt", ios::in); //3、打开文件并判断是否打开成功
if( !ifs.is_open())
{
cout << "文件打开失败" << endl;
return ;
}
//第一种 //4、读数据
char buf[1024] = {0};
while( ifs >> buf) //将文件ifs的内容输出到buf中
{
}
//第二种
char buf[1024] = {0};
while( ifs.getline(buf , seize(buf) ) ) //不断的去获取一行的数据
{
}
//第三种
string buf;
while( getline(ifs , buf) ) //直接将ifs文件的数据输出给buf
{
}
//第四种,不建议用,因为一个个字符去读,效率太慢了
char ch;
while( (c = ifs.get()) != EOF ) //一个字符一个字符去读,直到读取到文件尾部标志EOF为止
{
}
ifs.close(); 5、关闭文件
}
二进制文件
-
二进制文件比较强大,除了可以处理内置数据类型(int,char,double),还可以处理自定义数据类型
-
打开方式要额外指定:ios::binary
-
写文件方式主要利用流对象调用成员函数 write()
-
函数原型 : ostream& write ( const char * buffer, int len )
-
读文件方式主要利用流对象调用成员函数 read ()
-
函数原型:istream& read ( char * buffer, int len )
-
写文件
-
包含头文件:include<fstream>
-
创建流对象: ofstream ofs
-
打开文件并判断是否打开成功:ofs.open("文件路径", "打开方式")
-
读文件:创建类对象p,ofs.write( (const char*)&p , sizeof(类) )
-
关闭文件:ofs.close()
示例:
class Person
{
public:
char Name[64];
int Age;
}
#include <fstream> //1、包含头文件
int main()
{
ofstream ofs; //2、创建流对象
ofs.open("test.txt",ios::out | ios::binary); //3、打开文件
Person p("张三,18");
ofs.write( (const char *)&p, sizeof(Person) ); //4、写数据
ofs.close(); //5、关闭文件
}
-
读文件
-
包含头文件:include<fstream>
-
创建流对象: ifstream ifs
-
打开文件并判断是否打开成功:ifs.open("文件路径", "打开方式") if( !ifs.is_open() )
-
读文件:创建类对象p,ifs.read( (char*)&p , sizeof(类) )
-
关闭文件:ifs.close()
示例:
class Person
{
public:
char Name[64];
int Age;
}
#include <fstream> //1、包含头文件
int main()
{
ifstream ifs; //2、创建流对象
ifs.open("test.txt",ios::in | ios::binary); //3、打开文件并判断是否打开成功
if( !ifs.is_open() )
{
cout << "文件打开失败 " << endl;
return ;
}
Person p;
ifs.read( (char *)&p, sizeof(Person) ); //4、读数据
ifs.close(); //5、关闭文件
}
先更新到这儿吧,需要后面在补充。
希望以上内容可以帮助到大家。文章来源:https://www.toymoban.com/news/detail-529030.html
祝各位生活愉快。文章来源地址https://www.toymoban.com/news/detail-529030.html
到了这里,关于学习C++这一篇就够了(进阶篇)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!