目录
内存四区
- 代码区:存放二进制代码,并且只读、共享
- 全局区:
- 全局变量
- 静态变量 static
- 常量
- 字符串常量
- const修饰的常量
- const修饰的全局变量
- const修饰的局部常量-------栈区
- 栈区:由编译器分配释放
- 局部变量、形参(勿返回地址)
- 堆区:由程序员分配释放
// 分配---new关键字 1、创建变量 int *p = new int(10); // 创建一个值为10的变量并返回其地址 2、创建数组 int *p = new int[10]; // 创建一个包含10个元素的数组并返回其首地址 // 删除---delete关键字 delete p; delete[] p;
引用
-
给变量起别名
int a = 10; int b = &a;
- 注意事项:
- 引用必须初始化
- 初始化变量:初始值被拷贝到新建的对象中
- 初始化引用:程序将引用和它的初始值绑定在一起,而不是将初始值拷贝到引用
- 引用一旦初始化无法更改
- 一直绑定,无法令引用重新绑定到另一个对象
- 引用必须初始化
- 引用作函数参数------简化指针修改实参
// 值传递 void swap1(int a, int b) { int tmp = a; a = b; b = tmp; } // 地址传递 void swap2(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; } // 引用传递 void swap3(int &a, int &b) { int tmp = a; a = b; b = tmp; } int main() { int a = 10; int b = 20; swap1(a, b); // 值传递------无法修饰形参 swap2(&a, &b); // 地址传递------修饰形参 swap3(a, b); // 引用传递------修饰形参 return 0; }
- 引用做函数返回值
- 勿返回局部变量的引用
- 函数作左值
int& test() { static int a = 10; // 勿返回局部变量的地址 return a; } int main() { int &ref = test(); cout << ref << endl; // 10 test() = 1000; // 相当于赋值给a,而ref是a的别名,所以输出1000 cout << ref << endl; // 1000 }
- 引用本质
- 指针常量
int a = 10; int &b = a; // 自动转换为int* const b = &a; 指针常量 b = 20; // 内部发现b是引用,自动转换为*b = 20
- 指针常量
- 常量引用:修饰形参,防止误操作
函数提高
- 函数默认参数
- 传了参数用传的,没传用默认值
- 默认值放在后面
- if函数声明有默认值,函数实现就不能有默认参数------声明和实现只能有一个含默认值
- 函数占位参数
- 用于占位,调用函数时必须填补该位置
// 占位参数 void func(int a, int ) func(10, 10); // 占位参数还可以有默认值 void func(int a, int = 10)
- 用于占位,调用函数时必须填补该位置
- 函数重载:函数名相同,提高复用性
- 条件:
- 1、同一作用域下
- 2、函数名称相同
- 3、函数参数类型不同/个数不同/顺序不同
- 函数返回值不可以作为函数重载的条件
- 条件:
- 注意事项
- 引用作为重载条件
- 函数重载碰到函数默认参数------报错,有二义性
// 1、引用作为重载条件 void func(int &a) void func(const int &a) // 2、函数重载碰上函数默认参数 void func2(int a) void func2(int a, int b = 10) int main() { int a = 10; func(a); // 调用无const func(10); // 调用有const func2(10); // 报错,有二义性 return 0; }
类和对象
-
面向对象的三大特性:封装、继承、多态
-
c++认为万事万物皆为对象,对象上有其属性和行为
-
具有相同性质的对象,抽象为类
-
封装
- 意义
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
- class 类名{ 访问权限:属性/ 行为 };
- 属性------变量
- 行为------函数
- 注意
- 类中的属性和行为,统称为成员
- 属性(成员属性、成员变量)
- 行为(成员函数、成员方法)
- 属性和行为置于不同权限下加以控制
- public:公共权限------类内可以访问,类外可以访问
- protected:保护权限------类内可以访问,类外不可以访问
- private:私有权限------类内可以访问,类外不可以访问
- 注:protected和private存在继承方面的区别
- protected:子类可以访问父类中的保护内容
- private:子类不可以访问父类中的私有内容
- struct与class的区别:默认访问权限不同
- struct:公共权限
- class:私有权限
- 应用
- 将成员属性设为私有
- 将所有成员属性设为私有,可自己控制读写权限
- 对于写权限,可检测输入数据的有效性
- 行为中提供公共接口
- set------写权限
- get------读权限
- 将成员属性设为私有
对象的初始化和清理
-
对象
- 初始设置------构造函数
- 没有初始状态,后果未知
- 对象销毁前的清理数据的设置------析构函数
- 没有及时清理,造成安全问题
- 编译器自动调用,完成初始化和清理工作
- 强制使用,if不提供这两个函数,编译器会提供------但提供的是空实现
- 初始设置------构造函数
- 构造函数------类名 () {}
- 没有返回值也不写void
- 函数名称与类名一致
- 可以有参数,因此可重载overload
- 自动调用且只会调用一次------创建对象时
- 析构函数------ ~类名 () {}
- 没有返回值也不写void
- 函数名称与类名一致
- 不可以有参数,因此不可以overload
- 自动调用且只会调用一次------对象销毁前
-
构造函数的分类及调用
- 分类
- 按参数
- 有参构造
- 无参构造------默认构造
- 按类型
- 普通构造
- 拷贝构造
Person (const Person &p) { // 将传入的人身上的属性copy到本身 age = p.age; }
- 按参数
- 调用方式
- 括号法
- 显示法
- 隐式转换法
// 括号法 Person p1; // 无参调用 Person p2(10); // 有参调用 Person p3(p2); // 拷贝调用 // 显示法 Person p1; // 无参调用 Person p2 = Person(10); // 有参调用 Person p3 = Person(p2); // 拷贝调用 // 隐式转换法 Person p1; // 无参调用 Person p2 = 10; // 有参调用 Person p3 = p2; // 拷贝调用
- 注意:
- 调用默认构造函数时,不要加 () ------ Person p1();
- 编译器会认为是函数声明
- Person(10) :匿名对象------当前行执行后立即释放
- 不要利用拷贝构造函数初始化匿名对象
- 编译器会认为是对象声明
- 调用默认构造函数时,不要加 () ------ Person p1();
- 分类
-
拷贝构造函数调用时机
- 三种情况
// 1、使用一个已经创建完毕的对象来初始化一个新对象 Person p1(10); Person p2(p1); // 2、值传递的方式给函数参数传值 void do(Person p) { } void test() { Person p; do(p); } // 3、以值方式返回局部对象 Person do() { Person p1; return p1; } void test() { Person p = do(); }
- 三种情况
-
构造函数调用规则
- 创建一个类------默认情况下至少给该类添加三个函数
- 1、默认构造函数------空实现
- 2、析构函数------空实现
- 3、拷贝构造函数------用于对对象进行值拷贝
- 规则
有参构造函数 无参构造函数(默认) 拷贝构造函数 情况1 用户提供 √ c++提供 × √ 情况2 用户提供 √ c++提供 × ×
- 创建一个类------默认情况下至少给该类添加三个函数
-
深拷贝和浅拷贝
- deep copy------在堆区重新申请空间,进行拷贝操作
- shallow copy------简单的赋值拷贝操作
- 析构函数
- 将在堆区开辟的数据释放
~Person () { if(m_Height != NULL) { delete m_Height; m_Height = NULL; // 避免为野指针 } }
- 将在堆区开辟的数据释放
- if利用编译器提供的拷贝构造函数,会做shallow copy操作------Person p2(p1);
- 但是shallow copy带来的问题就是堆区的内存重复释放------deep copy解决
- 执行析构函数时,先释放p2,代码正常运行,堆区内存被释放。因此当析构p1时,由于此时堆区数据不存在,报错。
- 自己写一个拷贝构造函数------重新在堆区开辟一块内存
- if属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止shallow copy带来的问题
-
初始化列表
- 用来初始化属性
- 构造函数 ( ): 属性1(值1), 属性2(值2), ...{ }
// 1、传统初始化方式 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; Person p(10, 20, 30); // 2、初始化列表方式 // 2.1 赋值更改不方便 Person():m_a(10), m_b(20), m_c(30) { } int m_a; int m_b; int m_c; Person p; // 2.2 赋值自由 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; Person p(10, 20, 30);
-
类对象作为类成员
- 类中的成员可以是另一个类的对象,该成员称为对象成员
对象 实例 成员 属性+行为 对象成员 类中的成员是另一个类的对象 - eg:
- class A { } class B {A a;}
- B类中有对象A作为成员,A为对象成员
- 创建B对象时,A与B的构造和析构顺序?
- 当其他类对象作为本类成员,构造时先构造类对象,再构造本身;析构顺序与构造相反
- 类中的成员可以是另一个类的对象,该成员称为对象成员
-
静态成员
- 在成员变量和成员函数前加 static
静态成员变量 | 1、所有对象共享同一份数据 2、在编译阶段分配内存 3、类内声明,类外初始化 eg:static int a; int Person::a = 20; | 注: 1、不属于某个对象上,所有对象都共享同一份数据 2、两种访问方式: 通过对象------Person p; p.a; 通过类名------Person::a; 3、类外访问不到私有静态成员变量 |
静态成员函数 | 1、所有对象共享同一个函数 2、静态成员函数只能访问静态成员变量------无法区别到底是哪个对象的属性 | 注: 1、两种访问方式: 通过对象------Person p; p.func(); 通过类名------Person::func(); 2、类外访问不到私有静态成员函数 |
c++对象和this指针
-
成员变量和成员函数分开存储
- 在c++中,类内的成员变量和成员函数分开存储
- 只有非静态成员变量才属于类的对象上
// 空对象
class Person{};
Person p; // sizeof(p) = 1
// 1、空对象上占用内存1字节
// 2、c++编译器会给每个空对象分配一个字节空间---为了区分空对象占内存的位置
// 3、每个空对象应该有一个独一无二的内存地址
// 非静态成员变量
class Person{
int a;
};
Person p; // sizeof(p) = 4
// 静态成员变量
class Person{
int a;
static int b; // 不属于类的对象上
};
int Person::b = 10; // 静态成员变量---类内声明,类外初始化
Person p; // sizeof(p) = 4
// 非静态成员函数
class Person{
int a;
void func() {} // 不属于类的对象上
};
Person p; // sizeof(p) = 4
// 静态成员函数
class Person{
int a;
static void func() {} // 不属于类的对象上
};
Person p; // sizeof(p) = 4
-
this指针
- question
- c++成员变量和成员函数分开存储
- 每个非静态成员函数只会诞生一份函数实例---多个同类型对象共用一块代码
- 这块代码如何区分哪个对象调用自己?
- c++提供特殊的对象指针(this指针)---指向被调用的成员函数所属的对象
- this指针
- 概念
- 隐含在每个非静态成员函数内的一种指针
- 无需定义,直接使用
- 用途
- 当形参和成员变量同名时,可利用this指针区分---解决名称冲突问题
class Person { Person (int age) { age = age; // 冲突 } int age; } // 解决方法1 this->age = age; // 解决方法2 m_age = age; int m_age;
- 在类的非静态成员函数中返回对象本身------return *this;
void addage(Person &p) { this->age += p.age; } Person P1(10); Person P2(20); p2.addage(p1); // 可以正确输出 // 链式编程思想 p2.addage(p1).addage(p1); // 报错,返回值为void,即空对象无法调用成员函数 // 解决方法:返回对象本身 Person& addage(Person &p) { this->age = age; return *this; }
- 当形参和成员变量同名时,可利用this指针区分---解决名称冲突问题
- 概念
-
空指针访问成员函数
- c++空指针也可调用成员函数,但注意是否用到this指针
- if用到,需要加以判断来保证代码的健壮性
class Person { void showname() { cout << "this is my name!" << endl; } void showage() { cout << "age = " << m_age << endl; } int m_age; } void test01() { Person *p = NULL; p->showname(); // √ p->showage(); // × 默认有个this->m_age, 空指针访问报错 }
-
const修饰成员函数
- 常函数
- 定义:成员函数后加const---修饰的是this指针(指针常量:指针指向不可改),因此,再加个const,指针指向值不可改
- question:不可以修饰成员属性
- way:成员属性声明时加关键字mutable后,可修改
- 常对象
- 定义:声明对象前加const
- 常对象只能调用常函数
void showage() const {} // 常函数
const Person p; // 常对象
友元
- eg:客厅 public
- 卧室 private------可允许自己好朋友进去
- 定义:让一个函数或者类访问另一个类中私有成员 friend(关键字)
- 实现
// 全局函数做友元------全局函数访问类中私有变量 void goodGay() {} // 全局函数 在类中加入friend void goodGay(); // 类做友元------类访问另一个类中私有变量 class GoodGay {}; // GOodGay可访问Building私有成员 class Building { friend class GoodGay; } // 成员函数做友元------类成员函数访问另一个类中私有变量 class GoodGay { visit(); // 可访问Building私有成员 visit1(); // 不可访问Building私有成员 } class Building { friend void GoodGay::visit(); }
运算符重载
- 对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
-
加号运算符重载
- 实现两个自定义数据类型相加的运算
- 对于内置数据类型,编译器知道如何进行运算
// eg: int a = 10; int b = 10; int c = a + b;
- 非内置数据类型
- question
class Person { public: int m_a; int m_b; } Person p1; p1.m_a = 10; p1.m_b = 10; Person p2; p2.m_a = 10; p2.m_b = 10; Person P3 = p1 + p2; // 编译器不懂怎么计算
- way
// 1、自己写成员函数实现 Person add(Person &p) { Person tmp; tmp.m_a = this->m_a + p.m_a; tmp.m_b = this->m_b + p.m_b; return tmp; } // 编译器起了个通用名称 // 2、通过成员函数重载+号 Person operator+(Person &p) { Person tmp; tmp.m_a = this->m_a + p.m_a; tmp.m_b = this->m_b + p.m_b; return tmp; } // 调用时可以简化 Person p3 = p1.operator+(p2); Person p3 = p1 + p2; // 简化版本 // 3、通过全局函数重载+号 Person operator+(Person &p1, Person &p2) { Person tmp; tmp.m_a = p1->m_a + p2.m_a; tmp.m_b = p1->m_b + p2.m_b; return tmp; } // 调用时同样可以简化 Person p3 = operator+(p1, p2); Person p3 = p1 + p2; // 简化版本
- question
-
左移运算符重载
class Person { public: int m_a; int m_b; // 1、成员函数重载 << (左移运算符) void operator<<(Person &p) {} // × 需要两个对象 void operator<<(cout) {} // p.operator<<(cout) === p << cout // 不会利用成员函数重载<<运算符,因为无法实现cout在左侧 }; // 2、只能利用全局函数重载<<运算符 // void operator<<(cout, p) === cout << p // 但是有两个问题 // 2.1、需要先确定cout数据类型------右键→→→转到定义------ostream(输出流对象) // 2.2、返回值类型------链式编程思想,因此返回ostream ostream& operator<<(ostream &cout, Person &p) { cout << "m_a = " << p.m_a << "m_b = " << p.m_b; return cout; } void test01() { Person p; p.m_a = 10; p.m_b = 10; cout << p << endl; }
- 注:重载<<运算符配合友元可实现输出自定义数据类型
-
递增运算符重载
- 通过重载递增运算符实现自己定义的数据类型
// 1、重载前置++运算符 MyIntger& operator++() { num++; // 先进行++运算 return *this; // 再将自身返回------返回引用 } // 2、重载后置++运算符 MyIntger operator++(int ) { // int为占位参数,用于区分前置和后置 MyIntger tmp = *this; // 先记录 num++; // 再++运算 return tmp; // 最后返回记录值------返回值为局部对象 } MyIntger myint; ++myint; myint++;
- 注:前置++返回引用------为了对一个数据连续递增 ++(++myint)
- 后置++返回值
-
赋值运算符重载
- c++至少给一个类添加4个函数
- 默认构造函数
- 析构函数
- 拷贝构造函数
- 赋值运算符operator=,对属性进行值拷贝
- question
- if类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题------堆区内存重复释放
- way
Person& operator=(Person &p) { // 编译器提供的是shallow copy // m_age = p.m_age; // 应该先判断是否有属性在堆区,if有先释放干净再进行deep copy if(m_age != NULL) { delete m_age; m_age = NULL; } // deep copy m_age = new int(*p.m_age); // 返回对象本身 return *this; // 为了连续赋值 p1 = p2 = p3 }
-
关系运算符重载
- 实现两个自定义类型对象进行对比操作 < > == !=
bool operator==(Person &p) { if(this->m_name == p.m_name && this->m_age == p.m_age) { return true; } return false; } if(p1 == p2) {}
-
函数调用运算符重载
- 函数调用运算符()
- 因重载后使用的方式非常像函数的调用,称为仿函数
- 仿函数没有固定写法,非常灵活
void operator()(string test) { cout << test << endl; } // 两种方式调用 // 1、实例化对象 Person p; p("hello"); // 2、匿名函数对象 cout << Person()("hello"); // Person():匿名对象
继承
- 下级成员(上一级的共性+自己的特性)
- 继承目的------减少重复代码
// class 子类:继承方式 父类 // eg: class BasePage{}; class java : public BasePage{};
- 子类---派生类 父类---基类
-
继承方式
- 公共继承
- 保护继承
- 私有继承
-
继承中的对象模型
- question
- 从父类继承过来的成员,哪些属于子类对象中?
- conclusion
- 父类中所有非静态成员属性都会被子类继承
- 私有成员属性依然会被继承,但被编译器隐藏,因此无法访问
class Base { public: int a; protected: int b; private: int c; }; class Son : public Base { public: int d; } sizeof(Son); // 16
-
继承中构造和析构顺序
- 子类继承父类后,当创建子类对象,也会调用父类的构造函数
- question
- 父类和子类的构造和析构顺序
- conclusion
- 父类构造-子类构造-子类析构-父类析构
-
继承同名成员处理方式
- question
- 当子类与父类出现同名的成员,如何通过子类对象访问父类或子类中同名数据?
- way
Son s; s.m_a; // 访问子类同名成员------直接访问 s.Base::m_a; // 访问父类同名成员------加作用域
- 适用于成员属性和成员函数
-
继承同名静态成员处理方式
- question
- 继承中同名静态成员在子类对象上如何进行访问?
- conclusion
- 与非静态成员处理方式一致
- 同样适用于静态成员函数
// 两种访问方式 // 1、对象 s.m_a; s.Base::m_a; // 2、类名 Son::m_a; Son::Base::m_a;
-
多继承
- class 子类:继承方式 父类1,继承方式 父类2......
- 可能引发父类中同名成员情况出现,需加作用域区分
- c++实际开发中不建议多继承
-
菱形继承
- 两个派生类继承同一个基类,又有某个类同时继承两个派生类
- 菱形继承---钻石继承
- question
- 羊继承动物数据,驼同样继承动物数据,当羊驼使用数据时,出现二义性------加作用域
- 羊驼继承两份数据,但只需要一份数据------虚继承
// 虚继承:在继承前加关键字virtual // eg: class sheep : virtual public Animal {}; class tuo : virtual public Animal {};
- 类似共享同一份数据
- Animal类也称为虚基类
多态
- 分类
- 静态多态---函数重载、运算符重载属于静态多态,复用函数名
- 动态多态---派生类和虚函数实现运行时多态
- 区别
- 静态多态的函数地址早绑定---编译阶段确定函数地址
- 动态多态的函数地址晚绑定---运行阶段确定函数地址
class Animal {
virtual void speak() { // 加virtual变虚函数
cout << "animal is speaking" << endl;
}
};
class Cat : public Animal {
void speak() {
cout << "cat is speaking" << endl;
}
}
class Dog : public Animal {
void speak() {
cout << "dog is speaking" << endl;
}
}
void dospeak(Animal& animal) { // Animal& animal = cat 父类引用指向子类对象
animal.speak();
}
void test01() {
Cat cat;
dospeak(cat);
Dog dog;
dospeak(dog);
}
- 未加virtual(早绑定)
- animal is speaking
- animal is speaking
- 加virtual(晚绑定)
- cat is speaking
- dog is speaking
- 对象不同调用不同
- 条件
- 有继承关系
- 子类重写父类虚函数
- 重写:函数返回值类型、函数名称、参数列表完全相同
- 动态多态使用需要父类指针或引用指向子类对象
-
原理剖析
- 未加virtual---sizeof(Animal) = 1
- 加virtual---sizeof(Animal) = 4 (int、float、指针)
- 当父类指针或引用指向子类对象时,发生多态
- 优点
- 代码结构清晰
- 可读性强
- 利于前期和后期的扩展及维护
-
纯虚函数和抽象类
- question
- 在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
- way
- 将虚函数改为纯虚函数
- virtual 返回值类型 函数名 (参数列表) = 0;
- 当类中有了纯虚函数,这个类也称为抽象类
- 特点
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
-
虚析构和纯虚析构
- question
- 多态使用时,if子类中有属性开辟到堆区,那么父类指针在释放时无法调用子类的析构代码
- way
- 将父类中的析构函数改为虚析构或纯虚析构
- 共性
- 可解决父类指针释放子类对象
- 都需具体的函数实现
- 区别
- if是纯虚析构,该类属于抽象类,无法实例化对象
// 虚析构
virtual ~类名() {};
// 纯虚析构---声明+实现
virtual ~类名() {} = 0;
// 具体函数实现写在类外
类名::~类名 () {
// ...
}
文件操作
- question
- 程序运行时产生的数据(临时数据),运行结束即被释放
- way
- 通过文件将数据持久化 #include <fstream>
- 文件类型
- 文本文件---以文本的ASCII码形式存储在计算机中
- 二进制文件---以文本的二进制形式存储在计算机中
- 操作文件
- ofstream 写 o-output
- ifstream 读 i-input
- fstream 读写
文本文件
-
写文件
- 包含头文件 #include <fstream>
- 创建流对象 ofstream ofs;
- 打开文件 ofs.open("路径", 打开方式);
- 写数据 ofs << "写入的数据";
- 关闭文件 ofs.close();
- 打开方式
- ios::in 为读文件而打开
- ios::out 为写文件而打开
- ios::ate 初始位置:文件尾
- ios::app 追加写
- ios::trunc 文件存在删除再创建
- ios::binary 二进制形式
- 打开方式可配合使用,利用 | 操作符
- eg:ios::binary | ios::out
-
读文件
- 包含头文件 #include <fstream>
- 创建流对象 ifstream ifs;
- 打开文件并判断文件是否打开成功 ifs.open("路径", 打开方式);
- 读数据 四种方式读取
- 关闭文件 ifs.close();
- 文件是否打开成功
if(!ifs.is_open()) { cout << "文件打开失败" << endl; }
- 四种读数据方式
// 1 char buf[1024] = {0}; while(ifs >> buf) { cout << buf << endl; } // 2 char buf[1024] = {0}; while(ifs.getline(buf, sizeof(buf))) { cout << buf << endl; } // 3 string buf; while(getline(ifs, buf)) { cout << buf << endl; } // 4---不推荐 char c; while((c = ifs.get()) != EOF) { // EOF:End of file cout << c; }
二进制文件
- 以二进制的方式对文件进行读写操作
- 打开方式 ios::binary
-
写文件
- 二进制方式写文件主要利用流对象调用成员函数write
ostream& write(const char * buffer, int len); // 第一个参数:字符指针buffer指向内存中一段存储空间 // 第二个参数:读写的字节数
- 二进制方式写文件主要利用流对象调用成员函数write
-
读文件
- 二进制方式写文件主要利用流对象调用成员函数read
istream& read(char * buffer, int len);
- 二进制方式写文件主要利用流对象调用成员函数read