(注意:本阶段,主要分析 C++ 面向对象编程技术。)
一、内存分区模型
1、C++ 程序在执行时,将内存大致划分为4个区域:
(1)代码区:存放函数体的二进制代码,由操作系统进行管理的。
(2)全局区:存放全局变量和静态变量以及常量。
(3)栈区:由编译器自动分配和释放,存放函数的参数值、局部变量等。
(4)堆区:由程序员分配和释放,若程序员不释放,那么程序结束时,由操作系统回收。
2、内存分四区的意义:
不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程。
3、程序运行前:
在程序编译后,生成了 .exe 可执行程序,未执行该程序前,分为两个区域:
(1)代码区:存放 CPU 可执行的二进制机器指令。
代码区是“共享的”,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。
代码区是“只读的”,使其只读的原因,是防止程序意外地修改了它的指令。
(2)全局区:存放全局变量、静态变量。 —> 即:在函数体外的变量
全局区还包含了常量区、字符串常量、其它常量。该区域的数据在程序结束后,由操作系统释放。
(3)总结:
<1> C++中在程序运行前分为:全局区、代码区。
<2> 代码区特点是:共享、只读。
<3> 全局区中存放:全局变量、静态变量、常量。
<4> 常量区中存放:const 修饰的全局常量、字符串常量。
4、程序运行后:
(1)栈区:由编译器自动分配和释放,存放函数的参数值、局部变量等。
注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放。
(2)堆区:由程序员分配和释放,若程序员不释放,那么程序结束时,由操作系统回收。
注意:在C++中,主要利用new关键字,在堆区开辟内存。
示例:
int * func(){
// 利用new关键字,可以将数据开辟到堆区
int * p = new int(10); // 指针本质也是局部变量,放在栈区的,而指针保存的数据是放在堆区的。
return p;
}
int main(){
int * p = func(); // 在堆区开辟数据
cout << *p << endl;
system("pause");
return 0;
}
5、new 关键字:
C++中利用new关键字,在堆区开辟数据。堆区开辟的数据是由程序员手动开辟和释放的,释放是利用delete关键字。
(1)语法:new 数据类型;
(注:利用new关键字创建的数据,会返回该数据对应数据类型的指针。)
示例:
// 在堆区创建数据
void test01(){
int * p = new int(10); // new创建的数据,返回该数据对应数据类型的指针
cout << *p << endl;
delete p; // 由程序员释放该内存,使用delete关键字
cout << *p << endl; // 该段内存已经被释放,再次访问是非法操作,会报错!
}
// 在堆区开辟数组
void test02(){
int * arr = new int[10]; // 在堆区,创建一个数组
for(int i = 0; i < 10; i++){
arr[i] = i + 100; // 给数组赋值
cout << arr[i] << endl;
}
delete[] arr; // 释放数组,需要加[]
}
二、引用
1、基本概念
(1)作用:给变量起别名。
(2)语法:数据类型 &别名 = 原名;
示例:
int main(){
int a = 10;
int &b = a; // 创建引用
cout << "a = " << a << endl;
cout << "b = " << b << endl;
b = 100;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
system("pause");
return 0;
}
(3)注意事项:
<1> 引用必须要初始化!(例:int &b; // 这是错误的!)
<2> 引用一旦初始化后,就不可以更改了!
2、引用做函数参数
作用:函数传参时,可以利用引用的技术,让形参修改实参。
优点:可以简化指针修改实参。
示例:
// 1.值传递 --> 形参不会修改实参
void swap01(int a, int b){
int temp = a;
a = b;
b = temp;
}
// 2.地址传递 --> 形参会修改实参
void swap02(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
// 3.引用传递 --> 形参会修改实参
void swap03(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
3、引用做函数返回值 (注:不要返回局部变量的引用!)
用法:函数调用作为左值。
示例:
// 返回局部变量引用
int& test01(){
int a = 10; // 局部变量
return a;
}
// 返回静态变量引用
int& test02(){
static int a = 20; // 静态变量,存放在全局区,上面的数据在程序结束后,由系统释放
return a;
}
int main(){
int &ref1 = test01();
cout << "ref1 = " << ref1 << endl; // 第一次输出是正确的,是因为编译器做了保留
cout << "ref1 = " << ref1 << endl; // 第二次输出是错误的,是因为a的内存已经被释放了
int &ref2 = test02();
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl; // 第二次输出也是正确的,是因为a的内存没有被释放
test02() = 1000; // 函数调用可以作为左值,来进行赋值操作
cout << "ref2 = " << ref2 << endl;
system("pasue");
return 0;
}
4、引用的本质:引用在C++内部的实现,是一个指针常量(指针指向不可变,指针指向的值可以改变)
示例:
// 发现是引用,转换为:int * const ref = &a;
void func(int& ref){
ref = 100; // ref是引用,转换为:*ref = 100
}
int main(){
int a = 10;
int & ref = a; // 自动转换为:int * const ref = &a; 指针常量是指针指向不可改变,这就是引用一旦初始化后不可更改的原因
ref = 20; // 内部发现ref是引用,自动帮我们转换为:*ref = 20;
cout << "a:" << endl;
cout << "ref:" << endl;
func(a);
return 0;
}
总结:C++推荐使用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作,编译器都帮我们做了。
5、常量引用:主要是用来修饰形参,防止误操作的
在函数形参列表中,可以加const修饰形参,防止形参改变实参。
示例:
// 引用使用的场景,通常用来修饰形参
void showValue(const int& val){
val = 100; // 不可以修改,报错!
cout << "val = " << val << endl;
}
int main(){
int a = 10;
const int & ref = 10; // 加上const后,编译器将代码修改为:int temp = 10; const int & ref = temp; 此时值不可修改
showValue(a);
system("pause");
return 0;
}
三、函数提高
1、函数的默认参数
在C++中,函数形参列表中的形参是可以有默认值的。
语法:返回值类型 函数名(参数 = 默认值) { };
示例:
int func1(int a, int b = 10, int c = 10){ // 若某个位置参数有默认值,那么从这个位置之后,必须都要有默认值。
return a + b + c;
}
int func2(int a = 10, int b = 10); // 若函数声明有默认值,函数实现时就不能有默认参数。
int func2(int a, int b){
return a + b;
}
int main(){
cout << func1(10) << endl; // 因为该函数的形参列表中后面两个形参都有默认参数,所以只需要传递一个参数即可,即表示传参:a = 10
system("pause");
return 0;
}
2、函数占位参数
C++中函数的形参列表中可以有占位参数,用来做占位,调用函数时,必须填补该占位。
语法:返回值类型 函数名(数据类型) { };
示例:
void func(int a, int){ // 此处形参列表中,第二个数据类型就是占位参数,占位参数也可以有默认参数
cout << "this is func" << endl;
}
int main(){
func(10,10); // 此处必须填补占位参数
system("pause");
return 0;
}
3、函数重载:函数名可以相同,提高复用性
(1)函数重载满足条件:
1. 同一个作用域下
2. 函数名相同
3. 函数参数的类型、个数、顺序任意不同
注:函数的返回值不可以作为函数重载的条件。
(2) 注意事项:
1. 引用作为函数重载的条件
2. 函数重载碰到默认参数,会出现歧义,所以想用函数重载,就尽量避免默认参数。
四、类和对象
C++面向对象的三大特性为:封装、继承、多态。C++认为万事万物皆为对象,对象上有其属性和行为。
具有相同性质的对象,我们可以抽象称为:类。
1、封装
(1)意义:
<1> 设计一个类时,将属性和行为作为一个整体,表现一个事物。
语法:class 类名{ 访问权限:属性/行为 };
示例:
class Circle{
public: // 访问权限
int m_r; // 半径(属性)
double calculateZC(){ // 获取圆的周长(行为)
return 2 * PI * m_r;
}
};
int main(){
Circle c1; // 通过圆类,创建具体的圆对象 (即:实例化)
c1.m_r = 10; // 给圆对象的属性进行赋值
cout << "圆的周长为:" << c1.calculateZC() << endl; // 调用类中的行为,计算圆的周长
system("pause");
return 0;
}
<2> 设计一个类时,可以把属性和行为放在不同的权限下,加以控制。
三种访问权限:
public --> 公共权限(成员在类内、类外均可以被访问)
protected --> 保护权限(成员在类内可以被访问,但在类外不可以被访问)
private --> 私有权限(成员在类内可以被访问,但在类外不可以被访问)
(2)struct 和 class 的区别:
在C++中,他们唯一的区别是:默认访问权限不同( struct:默认为public;class:默认为private)。
(3)成员属性设置为 private
优点:可以自己控制读写权限。对于写权限,我们可以检测数据的有效性。
示例:
class Person{
public:
void setName(string name){ // 设置属性内容
m_Name = name;
}
void getName(){ // 获取属性内容
return m_Name;
}
private: // 成员属性都设置为私有的
string m_Name;
int m_Age;
};
int main(){
Person p;
p.setName("张三");
cout << "姓名为:" << p.getName << endl;
system("pause");
return 0;
}
注:在一个类中,可以让另一个类的对象作为这个类的成员变量。
(4)将不同类写在不同的文件中,并将其关联
<1> 首先编写一个 point.h 头文件,示例: —> 在头文件中写声明
#pragma once // 为了防止头文件重复
#include <iostream> // 提供标准的文件输入输出
using namespace std; // 标准的命名空间
class Point{ // 点类
public:
void setX(int x); // 头文件中的类,成员变量和函数只需要写出声明,不需要写实现
int getX();
void setY(int y);
int getY();
private:
int m_X;
int m_Y;
}
<2> 再编写一个 point.cpp 的源文件,示例: —> 在源文件中写实现
#include “point.h” // 表示包含前面编写的头文件
void Point::setX(int x){ // 源文件中直接写所包含头文件中类的实现即可,但是要注明作用域( 例:Point:: )
m_X = x;
}
int Point::getX(){
return m_X;
}
void Point::setY(int y){
m_Y = y;
}
int Point::getY(){
return m_Y;
}
<3> 在main()函数的源文件中,包含所需要的类的头文件即可,示例:
#include <iostream>
using namespace std;
#include "circle.h"
#include "point.h"
2、对象的初始化和清理
(1)构造函数和析构函数
对象的初始化和清理,是两个非常重要的安全问题。C++利用了构造函数和析构函数来解决该问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。若我们不提供这两个函数,则编译器会默认提供,编译器提供的构造函数和析构函数是空实现的。
<1> 构造函数:用于创建对象时,为对象的成员属性赋值,构造函数由编译器自动调用。
语法:类名( ) { }
说明:构造函数没有返回值也不写void;函数名与类名相同;可以有参数,因此可以发生重载;程序在调用对象时会自动调用,并且只会调用一次。
<2> 析构函数:用于对象销毁前,系统自动调用,执行一些清理工作。
语法:~类名( ) { }
说明:析构函数没有返回值也不写void;函数名与类名相同,在类名前面加上符号~;不可以有参数,因此不可以发生重载;程序在调用对象时会自动调用,并且只会调用一次。
示例:
class Person{
public:
// 构造函数
Person(){
cout << "Person的构造函数调用" << endl;
}
// 析构函数
~Person(){
cout << "Person的析构函数调用" << endl;
}
};
(2)构造函数的分类和利用
<1> 两种分类方式:
按参数分类:有参构造、无参构造(默认构造)
按类型分类:普通构造、拷贝构造
class Person{
public:
// 普通构造函数
Person(){
cout << "Person的无参构造函数调用" << endl;
}
Person(int a){
cout << "Person的构造函数调用" << endl;
}
// 拷贝构造函数
Person(const Person &p){
age = p.age; // 将传入类中的所有属性,拷贝过来
cout << "Person的拷贝构造函数调用" << endl;
}
};
<2> 三种调用方式:
括号法、显示法、隐式转换法
示例:
void test(){
// 1.括号法 --> 最常用
Person p1; // 默认构造函数的调用,注意:调用无参构造函数时,不要加()!因为编译器会把`Person p1(); `误认为是函数声明。
Person p2(10); // 有参构造函数的调用
Person p3(p2); // 拷贝构造函数的调用
Person(10); // 匿名对象,特点:当前执行结束后,系统会立即回收掉匿名对象。不要利用拷贝构造函数来初始化一个匿名对象!
// 2.显示法
Person p4 = Person(10);
Person p5 = Person(p4);
// 3.隐式转换法
Person p5 = 10;
Person p6 = p4;
}
(3)拷贝构造函数调用时机
C++中拷贝构造函数调用时机,通常有三种情况:
<1> 使用一个已经创建完毕的对象来初始化一个新对象
<2> 值传递的方式给函数参数传值
<3> 以值的方式返回局部对象
示例:
// 创建一个Person类
class Person{
public:
Person(){
cout << "无参构造函数!" << endl;
mAge = 0;
}
Person(int age){
cout << "有参构造函数!" << endl;
mAge = age
}
Person(const Person & p){
cout << "拷贝构造函数!" << endl;
mAge = p.mAge;
}
};
// 1.使用一个已经创建完毕的对象,来初始化一个新对象
void test01(){
Person p1(20);
Person p2(p1);
cout << "p2的年龄为:" << p2.mAge << endl;
}
// 2.以值传递的方式,给函数的参数传值
void doWork(Person p){ // 会拷贝一个新的Person对象,不会修改原始对象
}
void test02(){
Person p;
doWork(p);
}
// 3.以值的方式返回局部对象
void doWork2(){
Person p1;
return p1;
}
void test03(){
Person p = doWork2();
}
(4)构造函数的调用规则
默认情况下,C++编译器至少给一个类添加3个函数:
1.默认构造函数(无参);2.默认析构函数(无参);3.默认拷贝构造函数(对属性进行值拷贝)
构造函数调用规则如下:
<1> 若用户定义了有参构造函数,则C++不再提供默认的无参构造函数,但是会提供默认拷贝构造函数
<2> 若用户定义了拷贝构造函数,则C++不再提供其它构造函数
(5)浅拷贝和深拷贝
<1> 浅拷贝:就是简单的赋值拷贝操作(存在的问题:堆区的内存重复释放)
<2> 深拷贝:在堆区重新申请空间,进行拷贝操作(堆区开辟的数据,在析构函数中进行释放操作)
注:若属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。
(6)初始化列表
作用:C++提供了初始化列表的语法,用来初始化属性
语法:构造函数( ):属性1(值1), 属性2(值2), ...{ };
示例:
class Person{
public:
// 传统初始化操作
Person(int a, int b, int c){
m_A = a;
m_B = b;
m_C = c;
}
// 使用初始化列表来初始化属性
Person(int a, int b, int c): m_A(a), m_B(b), m_C(c){
}
int m_A;
int m_B;
int m_C;
};
(7)静态成员 —> 就是在成员变量、成员函数前加上static关键字
<1> 静态成员变量
1.所有对象共享同一份数据
2.在编译阶段分配内存
3.类内声明,类外初始化
<2> 静态成员函数
1.所有对象共享同一个函数
2.静态成员函数只能访问静态成员变量
示例:
class Person{
public: // --> 静态成员函数也是可以设置访问权限的(类外无法访问私有的静态成员函数)
// 静态成员函数
static void func(){
m_A = 100; // 静态成员函数可以访问静态成员变量
cout << "静态成员函数的调用" << endl;
}
// 静态成员变量
static int m_A;
};
int Person::m_A = 0; // 通过类名作用域,访问静态成员变量
// 静态成员函数有两种访问方式
void test(){
// 1.通过对象进行访问
Person p;
p.func();
// 2.通过类名进行访问
Person::func();
}
3、C++对象模型和 this 指针
(1)成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数是分开存储的,只有非静态成员变量才属于类的对象上。
(说明:空对象占用1个字节的内存空间,因为C++编译器会给每个空对象分配一个字节空间,为了区分不同的空对象占用内存空间的位置,每个空对象都有一个独一无二的内存地址。)
(2)this 指针概念
this指针指向被调用的成员函数所属的对象。this指针是隐含在每一个非静态成员函数内的一种指针。
作用:当形参和成员变量同名时,可用this指针来区分;在类的非静态成员函数中,需要返回对象本身,可使用:return * this
示例:
class Person(){
public:
// 构造函数
Person(int age){
this->age = age; // this指针,指向被调用的成员函数所属的对象
}
// 成员函数
Person& PersonAddAge(Person &p){ // 若返回值是对象本体,那么返回值类型就要用引用的方式
this->age += p.age;
return *this; // this指向的是p2的指针,*this指向的就是p2这个对象本体
}
// 成员变量
int age;
};
// 1.this指针解决名称冲突
void test01(){
Person p1(18);
cout << "p1对象的年龄:" << p1.age << endl;
}
// 2.使用*this,返回对象本身
void test02(){
Person p1(10);
Person p1(10);
p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1); // 由于该函数返回值是对象,因此可以多次调用(链式编程思想)
cout << "p2的年龄为:" << p2.age << endl;
}
(3)空指针访问成员函数
C++中空指针可以调用成员函数,但是要注意有没有用到this指针,若用到this指针,则需要进行判断,以保证代码的健壮性。
示例:
class Person{
public:
void showClassName(){
cout << "这是Person类!" << endl;
}
void showPersonAge(){
if(this == NULL){ // 判断该指针是否为空
return;
}
cout << "age = " << m_Age << endl;
}
int m_Age;
};
void test(){
Person * p = NULL;
p->showClassName();
p->showPersonAge();
}
(4)const修饰成员函数
<1> 常函数
1.成员函数后面加 const关键字后,称为常函数。
2.常函数内不可以修改成员属性。
3.成员属性声明时,加 mutable关键字后,在常函数中依然可以修改。
<2> 常对象
1.声明对象前面加 const关键字后,称为常对象。
2.常对象只能调用常函数。
示例:
class Person{
public:
void showPerson() const{ // 表示为常函数
m_A = 100;
}
int m_A;
mutable int m_B; // 表示为一个特殊的变量,此时在常函数中,可以对其进行修改
};
void test(){
const Person p; // 表示p为一个常对象
p.m_A =1000; // 此时会报错!因为常对象不能调用普通成员
p.m_B =1000; // 加了mutable关键字的变量,可以被常对象调用
p.showPerson(); // 常对象只能调用常函数,不能调用普通成员函数
}
(5)友元
作用:让一个函数或类访问另一个类中的私有成员。(关键字:friend)
三种实现:
<1> 全局函数做友元
示例:
class Building{
// 全局函数做友元
friend void goodGay(Building * building); // 表示goodGay()函数是友元全局函数,可以访问Building类中的私有成员
public:
Building(){
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
// 全局函数
void goodGay(Building * building){
cout << "好基友全局函数,正在访问:" << building->m_SittingRoom << endl;
cout << "好基友全局函数,正在访问:" << building->m_BedRoom << endl;
}
void test(){
Building building;
goodGay(&building);
}
<2> 类做友元
示例:
class Building; // 类的声明
class GoodGay{
public:
GoodGay();
public:
void visit(); // 成员函数,访问Building类中的属性
Building * building;
};
class Building{
friend class GoodGay; // 表示GoodGay类是本类的友元类,可以访问本类中的私有成员
public:
Building(); // 构造函数的声明
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
// 类外写构造函数
Building::Building(){class Person{
public:
void showPerson() const{ // 表示为常函数
m_A = 100;
}
int m_A;
mutable int m_B; // 表示为一个特殊的变量,此时在常函数中,可以对其进行修改
};
void test(){
const Person p; // 表抽象类示p为一个常对象
p.m_A =1000; // 此时会报错!因为常对象不能调用普通成员
p.m_B =1000; // 加了mutable关键字的变量,可以被常对象调用
p.showPerson(); // 常对象只能调用常函数,不能调用普通成员函数
}
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodGay::GoodGay(){
building = new Building; // 创建对象
}
void GoodGay::visit(){
cout << "好基友类,正在访问:" << building->m_SittingRoom << endl;
cout << "好基友类,正在访问:" << building->m_BedRoom << endl;
};
<3> 成员函数做友元
示例:
class Building; // 类的声明
class GoodGay{
public:
GoodGay();
public:
void visit(); // 让该成员函数,可以访问Building类中的私有成员
void visit2(); // 让该成员函数,不可以访问Building类中的私有成员
Building * building;
};
class Building{
friend void GoodGay::visit(); // 表示GoodGay类中的visit()函数,是本类的友元成员函数,可以访问本类中的私有成员
public:
Building();
public:
string m_SittingRoom;
private:
string m_BedRoom;
};
// 类外实现构造函数
Building::Building(){
m_SittingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodGay::GoodGay(){
building = new Building; // 创建对象
}
// 类外实现成员函数
void GoodGay::visit(){
cout << "visit()函数,正在访问:" << building->m_SittingRoom << endl;
cout << "visit()函数,正在访问:" << building->m_BedRoom << endl;
};
void GoodGay::visit2(){
cout << "visit2()函数,正在访问:" << building->m_SittingRoom << endl;
cout << "visit2()函数,正在访问:" << building->m_BedRoom << endl;
};
void test(){
GoodGay gg;
gg.visit();
gg.visit2(); // 会报错!visit()不是友元成员函数
}
(6)运算符重载
概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
<1> 加号运算符 + 重载:用于实现两个自定义数据类型相加的运算。
示例:
class Person{
public:
// 1.利用成员函数,重载+号运算符
Person operator+(Person &p){
Person temp;
temp.m_A = this->m_A + m_A;
temp.m_B = this->m_B + m_B;
return temp;
}
int m_A;
int m_B;
};
// 2.利用全局函数,重载+号运算符
Person operator+(Person &p1, Person &p2){
Person temp;
temp.m_A = this->m_A + m_A;
temp.m_B = this->m_B + m_B;
return temp;
}
void test(){
Person p1;
p1.m_A = 10;
p1.m_B = 10;
Person p2;
p2.m_A = 10;
p2.m_B = 10;
Person p3 = p1 + p2;
cout << "p3.m_A = " << p3.m_A << endl;
cout << "p3.m_B = " << p3.m_B << endl;
}
注:对于内置数据类型的表达式的运算符是不可以改变的。不要滥用运算符重载!
<2> 左移运算符 << 重载:可以输出自定义数据类型。
示例:
class Person{
public:
int m_A;
int m_B;
};
// 利用全局函数,重载<<号运算符
ostream& operator<<(ostream &cout, Person &p){
cout << "m_A = " << p.m_A << ", m_B = " << p.m_B;
return cout;
}
void test(){
Person p;
p.m_A = 10;
p.m_B = 10;
cout << p << endl;
}
注:重载左移运算符,配合友元,可以实现输出自定义数据类型。
<3> 递增运算符 ++ 重载:可以实现自己的整型数据。
示例:
// 自定义的整型
class MyInteger{
friend ostream& operator<<(ostream& cout, MyInteger myint); // 声明一个友元全局函数
public:
MyInteger(){
m_Num = 0;
}
// 前置++运算符重载
MyInteger& operator++(){ // 返回值类型为引用,表示一直对一个数据进行操作
m_Num++;
return *this; // 返回自身
}
// 后置++运算符重载
MyInteger operator++(int){ // 这里参数列表中的int,表示占位参数,可以用于区分前置和后置递增
MyInteger temp = *this; // 先记录当时结果
m_Num++;
return temp; // 返回之前记录的值。 后置递增返回值类型不能是引用
}
private:
int m_Num;
};
// 利用全局函数,重载<<号运算符
ostream& operator<<(ostream& cout, MyInteger myint){
cout << myint.m_Num;
return cout;
}
void test(){
MyInteger myint;
cout << ++myint << endl;
cout << myint++ << endl;
}
注:前置递增返回的是引用,后置递增返回的是值。
<4> 赋值运算符 = 重载:若类中有属性指向堆区,做赋值操作时,也会出现深浅拷贝的问题。
示例:
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){
// m_Age = p.m_Age; // 此时,编译器是提供浅拷贝,会报错!
// 应该先判断是否有属性在堆区。若有,先释放干净,然后再进行深拷贝。
if(){
delete m_Age;
m_Age = NULL;
}
m_Age = new int(*p.m_Age); // 此时,编译器进行深拷贝
return *this; // 返回对象本身
}
int *m_Age;
};
void test(){
Person p1(18);
Person p2(20);
Person p3(30);
p3 = p2 = p1; // 赋值操作
cout << "p1的年龄为:" << *p1.m_Age << endl;
cout << "p2的年龄为:" << *p2.m_Age << endl;
cout << "p3的年龄为:" << *p3.m_Age << endl;
}
<5> 关系运算符 == 重载:可以让两个自定义类型的对象进行对比操作。
示例:
class Person{
public:
Person(string name, int age){
m_Name = name;
m_Age = age;
}
// 关系运算符重载
bool operator==(Person &p){
if(this->m_Name == p.m_Name && this->m_Age == p.m_Age){
return true;
}
return false;
}
string m_Name;
int m_Age;
};
void test(){
Person p1("Tom", 18);
Person p2("Jerry", 18);
if(p1 == p2){
cout << "p1 和 p2 是相等的!" << endl;
}else{
cout << "p1 和 p2 是不相等的!" << endl;
}
}
<6> 函数调用运算符()重载:由于重载后的使用方式非常像函数的调用,因此称为 仿函数。它没有固定写法,非常灵活。
示例:
class MyPrint{
public:
// 函数调用运算符重载
void operator()(string str){
cout << str << endl;
}
};
void test(){
MyPrint myPrint;
myPrint("hello world"); // 由于重载后的使用方式非常像函数的调用,因此称为:仿函数。
}
(7)继承:可以减少重复代码
<1> 语法: class 子类名 : 继承方式 父类名{ };
( 例:class A : public B{ }; ) // 其中子类也叫做派生类,父类也叫做基类
<2> 继承方式:公共继承(public)、保护继承(protected)、私有继承(private)
说明:
1.父类中的私有成员,子类都无法访问。
2.公共继承中,父类中的公共、保护成员,到子类中依然是公共、保护权限。
3.保护继承中,父类中的公共、保护成员,到子类中都变成保护权限。
4.私有继承中,父类中的公共、保护成员,到子类中都变成私有权限。
<3> 继承中的对象模型
注:父类中所有非静态成员属性都会被子类继承下去,父类中私有成员属性是被编译器给隐藏了,因此访问不到,但是确实被继承下去。
<4> 继承中构造、析构的顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数。
顺序如下:
1.先构造父类,再构造子类。
2.先析构子类,在析构父类。
<5> 继承同名成员处理方式
当子类与父类之间,出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据:
1.访问子类中的同名成员,直接访问即可。
2.访问父类中的同名成员,需要加作用域。
示例:
// 父类
class Base{
public:
Base(){
m_A = 100;
}
void func(){
cout << "Base下的func()函数调用!" << endl;
}
void func(int a){
cout << "Base下的func(int a)函数调用!" << endl;
}
int m_A;
};
// 子类
class Son : public Base{
public:
Son(){
m_A = 100;
}
void func(){
cout << "Son下的func()函数调用!" << endl;
}
int m_A;
};
// 1.同名成员变量的处理方式
void test(){
Son s;
cout << "Son下的m_A = " << s.m_A << endl;
cout << "Base下的m_A = " << s.Base::m_A << endl; // 访问父类中的同名成员,需要加作用域
}
// 2.同名成员函数的处理方式
void test(){
Son s;
s.func();
s.Base::func(); // 访问父类中的同名成员,需要加作用域
s.Base::func(100); // 若子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数
// 此时若想访问父类中被隐藏的同名成员函数,需要加作用域
}
<6> 继承中同名静态成员处理方式
静态成员和非静态成员出现同名的处理方式一致,只不过有两种访问的方式(即:通过对象或类名进行访问)。
<7> 多继承:C++允许一个类继承多个类(一般不推荐使用!)
语法:class 子类 : 继承方式 父类1, 继承方式 父类2, ...{ };
(注:多继承中可能会引发不同父类中的同名成员出现,需要加作用域来区分。)
<8> 菱形继承
概念:两个派生类继承同一个基类,又有某个类同时继承两个派生类,这种继承被成为菱形继承(或钻石继承)。
产生的问题: —> 利用虚继承的方式,来解决!
1.羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
2.草泥马继承自动物的数据继承了两份(分别是羊和驼的数据),但是这两份数据我们只需要一份就可以了。
示例:
// 动物类
class Animal{
public:
int m_Age;
};
// 利用虚继承,来解决菱形继承的问题(即:在继承之前,加上virtual关键字),此时,最上面的Animal类称为:虚基类
// 羊类
class Sheep : virtual public Animal{ };
// 驼类
class Tuo : virtual public Animal{ };
// 羊驼类
class SheepTuo : public Sheep, public Tuo{ };
void test(){
SheepTuo st;
st.Sheep::m_Age = 18;
st.Tuo::m_Age = 28;
// 菱形继承时,两个父类拥有相同的数据,需要加作用域来区分。
cout << "羊的年龄:" << st.Sheep::m_Age << endl;
cout << "驼的年龄:" << st.Tuo::m_Age << endl;
// 加上虚继承之后,就只有一份数据了
cout << "羊的年龄:" << st.m_Age << endl;
}
(8)多态
分为两类:
<1> 静态多态:函数重载和运算符重载属于静态多态,复用函数名。
<2> 动态多态:派生类和虚函数实现运行时多态。
( 满足动态多态的条件:1. 有继承关系;2. 子类重写父类的虚函数。)
( 动态多态的使用:父类的指针或引用,指向子类对象。 )
区别:
<1> 静态多态的函数地址早绑定 ——> 在编译阶段就确定了函数地址
<2> 动态多态的函数地址晚邦定 ——> 在运行阶段才确定了函数地址
多态的好处:
<1> 组织结构清晰
<2> 可读性强
<3> 对于前期和后期扩展以及维护性高
(9)纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
纯虚函数的语法:virtual 返回值类型 函数名(参数列表)= 0;
// 当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
<1> 抽象类无法实例化对象。
<2> 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
(10)虚析构和纯虚析构
在多态使用时,若类中有属性开辟到堆区,那么父类指针在释放时,无法调用到子类的析构代码。
解决方式:将父类中的析构函数改为虚析构或纯虚析构。
两者共性:
<1> 可以解决父类指针释放子类对象。
<2> 都需要具体的函数实现。
两者区别:
<1> 若是纯虚析构,则该类属于抽象类,无法实例化对象。
虚析构函数的语法:virtual ~类名( ) { };
纯虚析构函数的语法:virtual ~类名( ) = 0; 类名::~类名( ) { };
总结:
<1> 虚析构函数或纯虚构函数,就是用来解决通过父类指针释放子类对象的问题。
<2> 若子类中没有堆区数据,可以不写为虚析构函数或纯虚析构函数。
<3> 拥有纯虚析构函数的类,也属于抽象类。
4、文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放。通过文件可以将数据持久化。
( 注意:C++中对文件操作,需要包含头文件:#include )
文件类型分为两种:
<1> 文本文件:该文件以文本的ASCII码形式存储在计算机中。
<2> 二进制文件:该文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂他们。
操作文件的三大类:
<1> ofstream:写操作
<2> ifstream:读操作
<3> fstream:读写操作
(1)文本文件
<1> 写文件(步骤)
1.包含头文件: #include
2.创建流对象:ofstream ofs;
3.打开文件:ofs.open(“文件路径”, 打开方式);
(注意:文件打开方式可以配合使用,利用 | 操作符,例如:用二进制方式写文件:ios::binary | ios::out )
4.写数据:ofs << “写入的数据”;
5.关闭文件:ofs.close();
<2> 读文件(步骤)
1.包含头文件: #include
2.创建流对象:ifstream ofs;
3.打开文件,并判断文件是否打开成功:ifs.open(“文件路径”, 打开方式); ( 注:利用is_open()函数,可以判断文件是否打开成功。)
4.读数据:四种方式读取
5.关闭文件:ifs.close();
(2)二进制文件
以二进制的方式对文件进行读写操作时,打开方式要指定为:ios::binary
<1> 写文件
二进制方式写文件,主要利用流对象调用成员函数 write()
函数原型:ostream& write(const char * buffer, int len);
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数。
<2> 读文件
二进制方式读文件,主要利用流对象调用成员函数 read()
函数原型:istream& read( char * buffer, int len);
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数。