Appearance
C++中级知识点
内存分区
内存分成四个区域:代码区、全局区、栈区、堆区。 程序运行前只有代码区、全局区
- 代码区:存放函数体的 二进制代码 ,由操作系统管理
- 全局区:存放 全局变量、静态变量以及常量
- 栈区:编译器自动分配释放,存放函数的参数值,局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
不同区域存放不同数据,有利于方便管理生命周期,给予更多编程灵活性
new操作符
new操作符用于在堆区开辟数据。由程序员管理开辟,也由程序员管理释放。释放操作可以用delete。
利用new创建的数据会返回该数据对应类型的指针
cpp
int* func(){
int* arr = new int[5];
int* p = new int(10);
// delete p; 该操作会释放p的地址空间,导致后续调用报错
// delete[] arr; 释放数组时要加中括号!!!切记切记!!!
return p;
}
引用
引用,用于给变量起别名,其本质是指针常量。
cpp
// 数据类型 &别名 = 原名
int a = 10;
int c = 20;
// 引用必须初始化,且不可再变更
int &ten = a; // 实际上是赋值了地址
cout << ten << endl; // 输出地址
cout << &ten << endl; // 输出地址
ten = c;
cout << a << endl; // 猜一下这三个输出什么
cout << b << endl;
cout << c << endl;
不要返回局部变量的引用
cpp
int& test01(){
int a = 10;
return a;
}
int main(){
int& ref = test01();
cout << ref << endl; // 结果由系统做了保留,但实际上已经失去了对地址的控制权
cout << ref << endl; // 因为失去了控制权,地址中的值会被系统利用,所以值会变化
// 省略system和return
}
cpp
int& test02(){
static int a = 10; // 静态变量,存放在全局区,在程序结束后由系统释放
return a;
}
int main(){
int& ref = test02();
cout << ref << endl;
cout << ref << endl;
test02() = 521;
cout << ref << endl;
cout << ref << endl;
// 省略system和return
}
常量引用
cpp
int& ref1 = 20; // 不加const不允许引用常量。此句会报错。
const int& ref = 10; // 加const之后,编译器会将代码视作:
// int temp = 10; const int& ref = temp; 也就是编译器会帮我们把常量创建成变量
函数进阶知识
默认参数
cpp
// 函数的形参列表中的形参是可以有默认值的
// 有传入用传入,无传入用默认
// 一个传入变量有默认,则后续传入变量都得有默认
// 声明里某个值提供了默认值,那么定义里不可以再给予默认值
// 返回值类型 函数名 (参数=默认值){}
int add(int a,int b=20,int c=30){
return a+b+c;
}
int swap(int,int) // 只提供类型起到占位作用。占位参数也可以有默认值
{
}
函数重载
函数名相同,提高复用性
需要满足的条件:
- 同一个作用域下
- 函数名称相同
- 函数参数 类型不同 或者 个数不同 或者 顺序不同
注意:函数的返回值不可以作为函数重载的条件
cpp
void func(){}
void func(int a){}
void func(int,float){}
// 引用作为重载条件
void func(int& a){} // int a = 10;func(a);
void func(const int& a){} // func(10);
// 函数重载遇到函数默认参数
void func(int b,int c = 10){}
void func(int b){} // 这两句在单参数调用时有二义性
面向对象
具有相同性质的对象,可以抽象为类。所以:
- 对象,是具体的类,是类的实例化
- 类,是诸多对象的抽象概念
面向对象有三大特征:封装、继承、多态
cpp
// 创建类
class Cube{ // 一个方盒类
public:
// 属性,简单理解为变量
int edge;
// 行为,理解为函数
int Volumn() {
return edge*edge*edge;
}
}; // 不要忽略这个分号
int main(){
Cube cube1;
cube1.edge = 10;
cout << "这是一个" << cube1.Volumn() << "立方米的立方体" << endl;
// 省略system和return
}
/*
类里的权限有三种:public protected private
public: 成员类内可以访问,类外也可以访问 公共权限
protected:类内可以访问,类外不可访问 保护权限
private: 类内可以访问,类外不可访问 私有权限
保护权限与私有权限的区别:
在后续的 “继承” 篇章中,类可以写成父子关系。private中的内容,子类不可访问
*/
struct和class的区别
这两者都能够构建出一个类,但struct默认权限为公共,class默认权限为私有
构造函数、析构函数
这两个函数必须成对出现。它们的作用分别是:
- 构造函数:在实例化对象时为成员属性赋值
- 析构函数:对象销毁前,释放成员占用的空间
在没有手动编写它们时,它们由编译器提供,但没有代码块在函数体内(即只是一个空函数)
cpp
class Cube{
/*
构造函数
格式:类名(){}
1、没有返回值也不写void
2、函数名称与类名相同
3、可以有参数,可以重载
4、调用对象时系统自动调用
*/
Cube(){
}
Cube(int length){
return length*10;
}
/*
析构函数
格式:~类名(){}
1、没有返回值也不写void
2、函数名称与类名相同,前面有~
3、没有参数,不可以重载
4、对象销毁前系统自动调用
*/
~Cube(){
}
// 拷贝构造 ( 其他非此种形式的叫做 普通构造 )
Cube(const Cube &c){}
}
void test01(){
Cube c1;
Cube c2 = Cube(10); // 有参构造
Cube c3 = Cube(c2); // 拷贝构造
Cube(22); // 匿名对象。特点:当前执行结束后,系统会立即回收匿名对象
// Cube(c3); // 不要利用拷贝函数初始化匿名对象 编译器会认为 Person(p3)===Person p3;即认为是对象声明
Cube c4 = 33; // 隐式转换法
Cube c5 = c4; // 隐式转换法的拷贝构造
}
void main(){
// test01();
Cube cube; // 可以尝试注释这行,调用test01(),析构函数的出现是不同的
//Cube cube(); // 此句会被认为是函数声明,而不是创建无参对象
// 省略system和return
}
// 析构函数的执行时机是 对象销毁之前。当在main中创建类,其销毁时机是return前,所以由于我们有system中断了程序执行,此时对象并没有释放,所以析构函数暂时不会出现。按任意按键之后,才能够走到return,才能够释放对象,也就可以执行析构了
拷贝构造函数
调用时机:
- 使用一个已经创建完毕的对象初始化一个新对象
- 值传递的方式给函数参数传值
- 值方式返回局部对象
cpp
class Person{
public:
Person(){
cout<<"Person默认构造函数调用" << endl;
}
Person(int age){
m_Age = age;
cout<<"Person有参构造函数调用" << endl;
}
Person(const Person &p){
m_Age = p.m_Age;
cout<<"Person拷贝构造函数调用" << endl;
}
~Person(){
cout<<"Person析构函数调用" << endl;
}
int m_Age;
}
void test01(){
Person p1(20);
Person p2(p1);
}
void test02(Person m){}
Person test03(){
return Person p;
}
深拷贝、浅拷贝
- 深拷贝:在堆区重新申请空间进行拷贝操作
- 浅拷贝:简单的赋值拷贝操作
初始化列表
cpp
// 构造函数():属性1(值1),属性2(值2),...{}
class Cube(){
Cube(): v_A(10),v_B(20),v_C(30){} // 构造函数时将属性初始化
// Cube(int a,int b,int c): v_A(a),v_B(b),v_C(c){} // 使用a、b、c替代写死的数可以更灵活
int v_A;
int v_B;
int v_C;
}
类对象作为类成员
cpp
class A{}
class B{
A a; // A是B的对象成员
}
静态成员
在成员变量、成员函数前加上static,则它们就变为静态成员变量,静态成员函数。
静态成员变量,所有对象共享同一份数据,编译阶段分配内存,类内声明,类外初始化
静态成员函数,所有对象共享同一个函数,静态成员函数只能访问静态成员变量
cpp
class Person{
public:
static int age; // 类内声明
}
int Person::age = 25; // 类外初始化
void test01(){
Person p;
cout << p.age << endl;
cout << Person::age << endl; // 由于静态成员不属于具体的对象,所以可以直接类名访问
}
this指针
每一个非静态成员函数只会诞生一份函数实例,即多个类型的对象会公用一块代码。那么如何区分哪个对象调用的呢?就需要使用this指针。
this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的本质是 指针常量,指针的指向不可更改
cpp
class Person{
public:
Person(int age){
this->age = age;
}
Person& addAge(Person &p){
if(this==NULL){ // 为防止空指针异常,可以加上这个判断块
return;
}
this->age += p.age;
// this指向p2的指针,*this指向p2这个对象本体
return *this;
}
int age;
};
void test01(){
Person p1(18); // this会指向p1.如果有其他对象,this也会分别指向那些对象
Person p2(10);
P2.addAge(p1).addAge(p1).addAge(p1); // 链式调用
}
const修饰成员
cpp
class Person{
Public:
// const Person * const this;
void showPerson() const // 常函数
{
this->m_A = 100;
}
int m_A;
mutable int m_B; // 特殊变量,即使在常函数中也可修改
};
void test01(){
const Person p; // 对象前加const变为常对象
p.m_A = 100; // 报错
p.m_B = 100; // 特殊值常对象下也可修改
p.showPerson(); // 常对象只能调用常函数
}
友元
能够访问类里的私有成员的特殊 函数 或 类 ,叫做友元函数、友元类。
cpp
// 友元函数
class Building {
friend void goodFriend(Building* building); // 友元函数声明
public:
Building() {
m_age = 18;
}
private:
int m_age;
};
void goodFriend(Building* building) {
cout << building->m_age << endl;
}
int main() {
Building bd;
goodFriend(&bd);
system("pause");
return 0;
}
cpp
// 友元类
class Building;
class GoodFriend {
public:
GoodFriend();
void visit();
Building* building;
};
class Building {
friend class GoodFriend; // 友元类
private:
int age;
};
GoodFriend::GoodFriend() {
building = new Building();
}
void GoodFriend::visit() {
building->age += 10;
cout << building->age << endl;
}
void test01() {
GoodFriend gf;
gf.visit();
}
cpp
// 友元成员
class Building;
class GoodBoy {
public:
GoodBoy();
void visit();
void visit2();
Building* building;
};
class Building {
friend void GoodBoy::visit();
private:
int m_age;
};
Building::Building(){
m_age = 10;
}
GoodBoy::GoodBoy(){
building = new Building;
}
void GoodBoy::visit() { building->m_age; }
void GoodBoy::visit2() {}
void test01(){
GoodBoy gb;
gb.visit();
gb.visit2();
}
运算符重载
对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
cpp
class Person{
public:
// 1、成员函数重载+号
// 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;
// }
int m_a;
int m_b;
};
// 2、全局函数重载+号
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;
}
void test01(){
Person p1;
p1.m_a = 10;
p1.m_b = 10;
Person p2;
p2.m_a = 10;
p2.m_b = 10;
Person p3 = p1 + p2;
}
继承
减少重复代码,提高复用
cpp
// class 子类(派生类) : 继承方式 父类(基类)
class Animal{
public:
void run();
}
class Cat:public Animal{} // 公共继承,保护继承,私有继承
/*
前提:父类中的私有成员子类默认不可访问
公共继承,父类中成员权限在子类中保持原样
保护继承,父类中的公共权限、保护权限在子类中皆变为保护权限
私有继承,父类中的公共权限、保护权限在子类中皆变为私有权限
*/
继承中的对象模型
提问:子类最终继承了父类中的哪些成员?
答案
父类中所有非静态成员都会被子类继承
私有属性被编译器隐藏,但其实是会被继承的
继承中构造和析构顺序
提问:构造顺序是怎样的?析构顺序呢?
答案
构造时,先父后子
析构时,先子后父
cpp
class Base {
public:
Base() { cout << "Base构造" << endl; }
~Base() { cout << "Base析构" << endl; }
};
class Son : public Base {
public:
Son() { cout << "son构造函数" << endl; }
~Son() { cout << "son析构函数" << endl; }
};
继承同名成员
提问:子类与父类有同名成员,如何通过子类访问该成员的数据呢?
答案
- 子类同名成员直接访问
- 父类同名成员加作用域
cpp
class Base {
public:
Base() {
m_a = 100;
}
void SayNum() {
cout << m_a << endl;
}
void SayNum(int inNum) {
this->m_a = inNum;
cout << m_a << endl;
}
int m_a;
};
class Son : public Base {
public:
Son() {
m_a = 200;
}
void SayNum() {
cout << m_a << endl;
}
int m_a = 200;
};
void test01() {
Son s;
cout << s.m_a << endl;
cout << s.Base::m_a << endl;
s.SayNum();
s.Base::SayNum();
// s.SayNum(10); // 会报错。
s.Base::SayNum(10);
}
/*
如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数
如果想访问父类中被隐藏的同名成员函数,需要加作用域
*/
继承同名静态成员
提问:继承中,如何访问同名的静态成员? 答案:与非静态成员的处理方式一致
cpp
// 静态成员可以直接类名访问
Base::m_a;
// 这种形式意味着:用Son类名,访问Base作用域下的m_a,即前后双冒号含义是不同的
Son::Base::m_a;
多继承
cpp
// class 子类 : 继承方式 父类1, 继承方式 父类2, ... {}
class Son : public Base1, public Base2{}
菱形继承
两个派生继承同一基类,同时,有某个类继承了这两个派生类
缺点:这种继承方式,不仅存在二义性,还造成内存空间的资源浪费
解决:虚继承
cpp
class Animal{ public: int m_age; }
class Tiger : virtual public Animal{}; // 继承方式前加 virtual 可变成虚继承
class Lion : virtual public Animal{};
class TigerLion : public Tiger, public Lion{}
多态
多态分为两类:
- 静态多态:函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态,派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定(编译阶段确定函数地址)
- 动态多态的函数地址晚绑定(运行阶段确定函数地址)
满足条件:
- 有继承关系
- 子类重写父类虚方法
动态多态使用: 父类的指针或者引用 指向子类对象
cpp
class Animal{
public:
// 虚函数
virtual void speak(){}
};
class Cat : public Animal{
public:
void speak(){}
};
void doSpeak(Animal &animal){
animal.speak();
}
void test01(){
Cat cat;
doSpeak(cat);
}
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现很少被调用,主要调用子类重写的内容,因此可以将虚函数改为 纯虚函数
如果一个类中有了纯虚函数,那么这个类就叫作 抽象类
抽象类:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
cpp
// virtual 返回值类型 函数名 (参数列表) = 0;
class Base{
public:
virtual void func() = 0;
}
class Son : public Base{
public:
virtual void func(){};
}
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
cpp
class Animal{
public:
virtual void speak() = 0;
};
class Cat:public Animal{
public:
Cat(string name){
m_Name = new string(name);
}
virtual void speak(){}
string* m_Name;
};
void test01(){
Animal* aimal = new Cat("Tom");
animal->speak();
delete animal;
}
C++文件操作
文件分成两种
- 文本文件,以ASCII码形式存储在计算机中
- 二进制文件,以二进制形式存储在计算机中
对文件操作,需要先包含头文件 <fstream>
cpp
/*
1. 包含头文件
#include <fstream>
2. 创建流对象
ofstream ofs;
3. 打开文件
ofs.open("文件路径", 打开方式); // 可以用 | 符号写多个打开方式4
4. 写数据
ofs << "写入的数据";
5. 关闭文件
ofs.close();
*/
打开方式
打开方式 | 解释 |
---|---|
ios::in | 读文件 |
ios::out | 写文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加 |
ios::trunc | 存在则先删除,再创建 |
ios::binary | 二进制方式 |
文本文件——写入
cpp
ofstream ofs;
ofs.open("test.txt", ios::out);
ofs << "123123";
ofs.close();
文本文件——读取
cpp
ifstream ifs;
ifs.open("test.txt", ios::in);
if (!ifs.is_open()) {
return;
}
// 第一种方法
/*char buf[1024] = { 0 };
while (ifs >> buf) {
cout << buf << endl;
}*/
// 第二种方法
/*char buf[1024] = { 0 };
while (ifs.getline(buf, sizeof(buf))) {
cout << buf << endl;
}*/
// 第三种方法
/*string buf;
while (getline(ifs, buf)) {
cout << buf << endl;
}*/
// 第四种
//char c;
//while ((c = ifs.get()) != EOF) { // EOF = End of File
// cout << c;
//}
ifs.close();
二进制——写入
cpp
class Person {
public:
int age;
void initAge();
};
void Person::initAge() {
age = 10;
}
void test01() {
ofstream ofs;
ofs.open("testBinary.txt", ios::out|ios::binary);
Person p;
p.initAge();
ofs.write((const char*)&p, sizeof(Person));
ofs.close();
}
二进制——输出
cpp
ifstream ifs;
ifs.open("testBinary.txt", ios::in | ios::binary);
if (!ifs.is_open()) {
return;
}
Person p;
ifs.read((char*)&p, sizeof(Person));
cout << p.age;
ifs.close();