C++核心编程——面向对象
内存模型
- 代码区:存放函数的二进制代码,由操作系统进行管理
- 代码区是共享的
- 代码区是只读的
- 全局区:存放全局变量和静态变量以及常量
- 栈区:(程序运行后)由编译器自动释放,存放函数的参数值,局部变量等
- 函数不要返回局部变量的地址,因为程序运行结束后局部变量就被释放了。
- 堆区:由程序员分配释放,若不释放,程序结束后由os回收
- 主要用new在堆区开辟内存:
int *p = new int(10)
- 由delete区释放内存:
delete P; delete[] arr;
- 主要用new在堆区开辟内存:
引用
-
作用:给变量起别名,本质是一个指针常量:
int* const b = &a;
-
语法:数据类型 &别名 = 原名:
int a = 10; int &b = a; // 即给一个已有地址索引a的数据内存10创建一个新的地址索引b
-
注意事项:
- 引用必须初始化(先给a,再有引用b)
- 引用初始化后不可更改(索引更改)
- 不要返回局部变量引用
-
函数传参时,可以利用引用的方法让形参修饰实参
void swap(int &m, int &n); //相当于给a, b起了一个别名m, n int a, b; swap(a, b);
函数高级
-
默认参数
-
语法:
int func(int a, int b = 9, int c = 7){}
-
注意事项:
(1)如果函数名中某个位置有默认参数,则这个位置之后的所有参数都必须有默认参数
(2)如果函数申明中有默认参数,则函数实现就不能由默认参数
int func(int a, int b, int c){ // 则函数实现就不能由默认参数 }
-
-
函数的占位参数
-
C++中函数的形参列表中可以用占位参数来占位,调用函数时必须填补该位置
-
语法:返回值类型 函数名 (数据类型){}
-
代码实现:
void func(int a, int){} // 第二个位置的int就是占位参数 int main() { fun(10, 20) }
-
-
函数重载
-
函数名可以相同,提高复用行
-
重载满足条件:
(1)同一个作用域下
(2)函数名称相同
(3)函数参数类型不同或者个数不同或者顺序不同
-
注意事项:函数的返回值不可以作为函数重载条件
-
语法:
void func(){}; void func(int a){}; fun(); // 调用第一个func func(10); // 调用第二个func
-
面向对象——类和对象
- C++面向对象三大特性:封装、继承、多态,万物皆为对象,对象都有其属性和行为
封装
-
封装的意义:
-
将属性和行为作为一个整体,表现生活中的事物
(1)语法:class 类名{ 访问权限:属性 / 行为}
(2)代码实现:
class cicle{ public: //访问权限 int radius; //圆的属性 double calculateZC(); //计算周长(行为) }
-
将属性和行为加以权限控制
(1)访问权限由三种:public(公共权限)、protected(保护权限)、private(私有权限)
- public:成员在类内和类外都可以访问
- protected:成员在类内可以访问,类外不可以访问(继承中父类的保护内容子类也可以访问)
- private:成员在类内可以访问,类外不可以访问(继承中父类的私有内容子类不可以访问)
-
struct和class的区别:class的默认权限时私有,struct的默认权限时公共
-
成员属性设置为私有可以控制读写权限或者对于写可以检测数据的有效性
class person { private: //将成员属性设置为私有可以控制读写权限 int name; int age; public: // 然后通过设置属性的操作函数来对属性进行读写 void setage(int age){}; void getname(int name){}; }
-
对象初始化与清理
-
利用构造函数和析构函数进行对象的初始化和清理,这两个函数由编译器强制自动调用,若不写,编译器会自动调用空实现的两个函数
-
构造函数:类名(){}
(1)构造函数,没有返回值也不写void
(2)函数名和类名相同
(3)构造函数里可以有参数,因此可以重载
(4)程序调用对象时会自动调用析构,无需手动调用,而且只会调用一次
class Person { Person(){} // 构造函数 ~Person(){} // 析构函数 } // 调用 test() { Person P; }
-
析构函数:~类名(){}
(1)构造函数,没有返回值也不写void
(2)函数名和类名相同,在名称前加~
(3)构造函数里不可以有参数,因此不可以重载
(4)程序调用对象时会自动调用析构,无需手动调用,而且只会调用一次
构造函数的分类与调用
-
分类:有参构造和无参构造(默认)、普通构造和拷贝构造
class Person { public: Person(const Person &P){} // 拷贝构造函数 }
-
调用方法:括号法、显示法、隐式转换法
test() { Person P(10); // 括号法 Person P2=Person(10); // 显示法 Person P3 = 10; // 隐式调用法 }
-
拷贝构造函数的调用时机:
(1)使用一个已经创建好的对象来初始化一个新对象
(2)值传递的方式给函数参数传值
(3)以值方式返回局部对象
-
构造函数调用规则:
默认情况下,c++编译器至少给一个类添加3个函数,若用户已经自定义,则不再提供
(1)默认构造函数(无参数,函数体为空)
(1)默认析构函数(无参数,函数体为空)
(1)默认拷贝构造函数,拷贝属性
-
深拷贝与浅拷贝
(1)深拷贝:在堆区重新申请空间,进行拷贝操作
(2)浅拷贝:简单的赋值拷贝操作(容易导致堆区的内存重复释放)
-
类的初始化
(1)语法:构造函数(): 属性1(值1),属性2(值2)…{}
Person(int a, int b): A(a), B(b){} Person(20, 30, 19); // 参数初始化调用
-
类对象作为成员
(1)C++中A类中的对象可以是另一个类B的成员
(2)构造调用时会先调用A类后调用B类,析构调用时先释放B,后释放A
-
静态成员函数
(1)所有对象共享同一个函数
(2)静态成员函数只能访问静态成员变量
static void func(){ A = 100; // 可以调用 B = 200; // 错误,不能访问,不能区分到底属于那个对象的参数 } static int A; int B;
对象模型和this指针
-
成员变量和成员函数分开储存
- 只有非静态成员变量才属于类的对象
- 语法:编译器会给每个空对象分配一个字节的内存空间,是为了区分空对象占内存的位置,每个空对象都有一个独一无二的内存地址
-
this指针
-
this 指针指向被调用的成员函数所属的对象,他隐含每一个非静态成员函数的一种指针,不需要定义直接使用
-
用途:解决形参与成员变量同名时,用this区分;在类的非静态成员函数中返回对象本身,return *this
class Person { Person(int age){ this->age = age; } int age; }
-
-
空指针访问成员函数
-
const修饰成员函数:为常函数,常函数内不可以修改成员属性
-
成员属性声明时夹关键字mutable后,在常函数中依然可以修改
-
声明对象前加const称为常对象
-
常对象只能调用常函数
class Person { public: Person(int age) const { this->age = age; // 该语法错误,const修饰后无法修改成员属性 this = NULL; // 语法错误,this指针本质为指针常量,其指向不能修改 this->m_b = 100; // 可以修改 } void func(){} int m_age; mutable int m_b } const Person P; // 在对象前加const,变为常对象 P.Person(); // 常对象只能调用常函数,即不能调用func函数
-
友元
-
在程序中有些私有属性,可以让类外的特殊的函数或者类进行访问,关键字:friend
-
三种实现方式:
-
全局函数做友元
class Building { friend void goodfriend(Building *building); // 可以通过friend修饰的全局函数访问私有成员 public: string m_sittingroom; private: string m_bedroom; } // 全局函数 void goodfriend(Building *building){}
-
类做友元
class Building { friend class goodgay; // 可以通过friend修饰的类访问私有成员 public: string m_sittingroom; private: string m_bedroom; } class goodgay { public: void visit(); Building *building; }
-
成员函数做友元
class Building { friend void goodgay::visit(); // 可以通过friend修饰的成员函数访问私有成员 public: string m_sittingroom; private: string m_bedroom; } class goodgay { public: void visit(); Building *building; }
-
运算符重载
- 对已有的运算符进行重新定义,赋予另一种功能,以适应不同的数据类型
-
加号运算符重载
-
实现两个自定义数据类型相加的运算:成员函数和全局函数均可实现
class Person{ public: Person operator+(const Person &p){ Person temp; temp.a = this->a + p.a; temp.b = this->b + p.b; return temp; } } Person p3 = p1 + p2; // 运算符重载可以发生函数重载 Person operator+(const Person &p, int val){}
-
-
左移运算符重载:可以输出自定义类型,全局函数均可实现、
class Person{ public: Person operator<<(ostream &cout); // 此时的调用方法应是 p << ciut, 与实际不符合,因此通常不用成员函数重载 } // 只能用全局函数重载左移运算符 ostream & operator<<(ostream &cout, Person &p){ cout << .....; return cout; // 通过链式编程无限向后追加内容例如可以 cout << p << endl; 若是没有return cout, endl项会报错 } cout << p << endl;
-
递增运算符重载:通过重载,实现自己的整型变量
class MyInt { public: myint(){ m_num = 0; } // 重载前置++运算符 MyInt& operator++() { m_num++; return *this; // 返回自身引用,与直接返回值不同,返回引用一直对一个数据进行递增操作; } // 重载后置++运算符,此时会发生函数重定义错误,可以通过加int占位参数来区分 MyInt operator++(int) { MyInt temp = *this; m_num++; return temp; // 返回值; } private: int m_num; }
-
赋值运算符重载:防止堆区内存重复释放
-
关系运算符重载:让两个自定义类型对象进行比较: ==,!=
bool operator==(){}; bool operator!=(){};
-
函数调用运算符重载:()调用运算符重载后由于使用方式非常像函数调用,因此成为仿函数,无固定写法
void operator()(string test) // 第一个括号时函数重载运算符 { cout << test << endl; } myprint("hello!"); //此处的括号即是函数调用运算符重载后的符号 Person()("hello"); // 类名+()为匿名函数对象,用完即释放
继承
-
语法:减少代码重复量
- class 子类(派生类):继承方式 父类(基类)
-
继承方式
-
公共继承
性质:父类的私有内容子类不可访问,父类其他内容属性形式不变
-
保护继承
性质:父类的私有内容子类不可访问,父类保护权限和公共权限内容均变为保护权限
-
私有继承
性质:父类的私有内容子类不可访问,父类保护权限和公共权限内容均变为私有权限
-
-
继承中的对象模型
从父类中继承的成员,哪些属于子类的对象中
- 父类中所有的非静态成员属性都会被子类继承,包括私有属性,虽然无法访问
-
继承中构造和析构的顺序
子类继承父类后,创建子类对象时也会调用父类的构造函数创建父类对象,且顺序如下:先构造父类,再构造子类。析构顺序和构造相反。
-
继承中同名成员的处理方式
- 访问子类同名成员,直接访问即可(类名.成员)
- 访问父类同名成员,需要加作用域:Person.Base::func()(Base是父类的类名)
- 若子类出现和父类同名的成员函数,子类的同名成员会隐藏父类中所有的同名函数
-
继承中同名静态成员的处理方式
- 访问子类同名成员,直接访问即可(类名.成员)
- 访问父类同名成员,需要加作用域:Person.Base::a(Base是父类的类名)或者通过类名访问静态成员:Person::Base::a(第一个:: 代表通过类名访问静态成员,第二个:: 代表同名父类的作用域
-
多继承语法——c++实际开发不建议使用
语法:class 子类:继承方式 父类1,继承方式 父类2,…
多继承可能会出现很多同名成员的情况,需要加作用域访问,容易出错
-
菱形继承
- 概念:
- 两个派生类继承同一个基类
- 又有某一个类同时继承这两个派生类
- 这种继承被称为菱形继承
- 菱形继承的问题
- 会产生继承属性的二义性:通过添加作用域解决
- 部分相同的属性会继承两份,浪费空间:利用虚继承解决:virtual public 类名(此时继承的是虚基类指针)
- 概念:
多态
-
基本概念
- 分类
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
- 静态与动态多态的区别
- 静态多态的而函数地址早绑定-编译阶段确定函数地址
- 动态多态的而函数地址晚绑定-运行阶段确定函数地址
- 动态多态满足条件
- 有继承关系
- 子类要重写父类虚函数(重写:函数返回值 函数名 参数列表完全相同;虚函数:virtual void func())
- 动态多态使用
- 父类的指针或者引用指向子类对象
-
多态的优点
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的维护和拓展
案例语法:
class cal { public: virtual int getres() { return 0; } int num1; int num2; } // 多态L:减法和乘法等类的写法相同 class add : public cal { public: int getres() { return num1 + num2; } int num1; int num2; } // 父类指针引用子类对象 cal * abc = new add; delete abc;
-
纯虚函数和抽象类
- 在多态中,通常父类的虚函数实现毫无意义,主要调用子类重写的内容,因此可以将虚函数改为纯虚函数
- 纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0;
- 当类中有纯虚函数,这个类也被成为抽象类
- 抽象类特点:无法实例化对象;子类必须重写抽象类中的纯虚函数,否则也属于抽象类
-
虚析构和纯虚析构
- 多态使用时,如果子类中有属性开辟堆区,那父类指针在释放时无法调用到子类的析构代码
- 解决方法:将父类的析构函数改为虚析构和纯虚析构
- 虚析构语法:virtual ~函数名()
- 纯虚析构需要声明也需要具体实现
- 分类
文件操作
- 头文件
- 文本文件——文件以文本的ASCII码形式储存
- 二进制文件——文件以文本的二进制形式储存
- 操作文件三大类:写文件ofstream,读操作ifstream,读写操作fstream
-
文本文件
-
写文件步骤如下
(1) 包含头文件:
#include<fstream>
(2) 创建流对象:
ofstream ofs
(3) 打开文件,
ofs.open("文件路劲", 打开方式)
(4) 写数据:
ofs << 写入数据
(5) 关闭文件:
ofs.close()
文件打开方式:
-
读文件步骤:
(1) 包含头文件:
#include<fstream>
(2) 创建流对象:
ifstream ifs
(3) 打开文件,
ifs.open("文件路劲", 打开方式)
(4) 读数据:四种数据读取方式
(5) 关闭文件:
ifs.close()
-
-
二进制文件
- 写文件函数:
ostream& write(const char * buffer, int len)
- 读文件操作:
istream& read(char * buffer, int len)
- 写文件函数: