C++核心编程
1、c++内存模型
c++程序执行时将内存大致分为4个区域
- 代码区:存放CPU执行的二进制代码指令,由操作系统进行管理
- 全局区:存放全局变量和静态变量以及全局常量和字符串常量
- 栈区:由编译器自动编译释放,存放函数的参数值,局部变量等
- 堆区:由程序员分配释放,如果程序员不释放,则程序结束时由操作系统回收
c++内存模型图如下:
?
代码区特点
共享的:对于频繁执行的程序,在内存中只要有一份代码即可
只读的:防止系统意外修改它的指令
全局区特点
该区域的数据,在程序结束后由操作系统释放
栈区特点
由编译器自动编译释放,存放函数的参数值,局部变量等
注意事项:栈区编译的数据执行完后由编译器自动释放,不要返回局部变量的地址
堆区特点
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
在c++中主要通过new关键字在堆区开辟内存
new 和delete关键字
new关键字开辟内存
elete关键字释放指针p指向的内存
int* func()
{
int * p = new int(21);
return p;
}
int main()
{
int * p = func();
cout << *p << endl;
delete p;
system("pause");
return 0;
}
2、引用
引用可以看做是变量的一个别名,通过这个别名和原来的名字都能够找到这个变量。
语法:
int a = 10;
int &b = a;
注意事项:引用必须在定义的同时初始化,并且以后也要从一而终,不能再引用其它变量。
引用作为函数参数
将函数的形参指定为引用的形式,这样在调用函数时就会将实参和形参绑定在一起,让它们都指代同一个变量。如此一来,如果在函数体中修改了形参的值,那么实参的值也会被修改。
void swap(int &a1,int &b1)
{
int temp = a1;
a1 = b1;
b1 = temp;
}
int main()
{
int a = 10;
int b = 20;
swap(a, b);
cout << a << "\n" << b << endl;
system("pause");
return 0;
}
引用作为函数返回值
不能返回局部数据(例如局部变量、局部对象、局部数组等)的引用,因为当函数调用完成后局部数据就会被销毁,有可能在下次使用时数据就不存在了,C++ 编译器检测到该行为时也会给出警告。
int& test() {
static int a = 10;
return a;
}
int main()
{
int &ref = test();
cout << ref << endl;
test() = 1000;
cout << ref << endl;
system("pause");
return 0;
}
引用的本质
引用的本质是一个指针常量,所有指针操作由编译器完成。
常量引用
常量引用主要用来修饰形参,防止误操作;
在函数中可以用const修饰形参,防止形参改变实参
void showValue(const int& a1) {
cout << a1 << endl;
}
int main()
{
int a = 10;
showValue(a);
system("pause");
return 0;
}
3、函数高级
函数默认参数
在c++中函数的形参列表中的参数是可以有默认值的;
在函数中如果某个参数有默认值,那么它后面的所有参数都得有默认参数;
如果函数声明有默认参数,那么函数的定义就不能有默认参数
int func01(int a, int b = 10, int c = 10) {
return a + b + c;
}
int func02(int a, int b = 10);
int func02(int a, int b) {
return a + b;
}
int main()
{
cout << func01(1, 4) << endl;
cout << func01(1) << endl;
cout << func02(1) << endl;
system("pause");
return 0;
}
函数占位参数
c++中函数的形参列表可以有占位参数,用来占位,调用函数时必须填补该位置
占位参数还可以有默认值
void func(int a, int = 5) {
cout << "This is a function " << endl;
cout << a << endl;
}
int main() {
func(12, 9);
system("pause");
return 0;
}
函数重载
重载:函数名称相同,函数参数类型不同或者个数不同或者顺序不同
void func() {
cout << "This is a function 01 " << endl;
}
void func(int a) {
cout << "This is a function 02" << endl;
}
void func(double a) {
cout << "This is a function 03" << endl;
}
void func(int a,double b) {
cout << "This is a function 04" << endl;
}
void func(double a, int b) {
cout << "This is a function 05" << endl;
}
注意事项:
- 引用可以作为重载条件
- 重载时避免出现默认参数,会出现二义性
4、面向对象
1、封装
封装
将属性和行为作为一个整体,表现生活中的事务
将属性和行为加以权限控制
成员变量和成员函数
class Student{
public:
char *name;
int age;
float score;
void say(){
cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
}
};
成员变量和普通变量一样,也有数据类型和名称,占用固定长度的内存。但是,在定义类的时候不能对成员变量赋值,因为类只是一种数据类型或者说是一种模板,本身不占用内存空间,而变量的值则需要内存来存储。
成员函数是一个类的成员,出现在类体中,它的作用范围由类来决定;而普通函数是独立的,作用范围是全局的,或位于某个命名空间内。
权限
类成员可以被定义为 public、private 或 protected。默认情况下是定义为 private。
C++ 中的 public、private、protected 只能修饰类的成员,不能修饰类,C++中的类没有共有私有之分。
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。
struct 和 class 的区别
区别:
属性私有
优点:
- 将所有成员属性设置为私有,可以自己控制读写权限
- 对于写权限,我们可以检测数据的有效性
2、对象初始化和清理
构造函数
主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用
class Person {
public:
int m_age;
Person() {
cout << "Person的无参构造" << endl;
}
Person(string name,int age) {
m_age = age;
cout << "Person的有参构造" << endl;
}
Person(const Person &p) {
m_age = p.m_age;
cout << "Person的拷贝构造" << endl;
}
};
析构函数
主要作用在于对象销毁前由系统自动调用,执行一些清理工作,
程序在对象销毁前会自动调用析构,无需手动调用,而且只会调用一次
class Person {
public:
int m_age;
~Person() {
cout << "Person的析构函数" << endl;
}
};
深拷贝和浅拷贝
浅拷贝:简单的赋值拷贝
深拷贝:在堆区重新申请空间,进行拷贝工作
浅拷贝带来的问题:堆区的内存重复释放
深拷贝解决:自己提供拷贝构造函数,给拷贝的副本重新开辟内存空间
Person(const Person &p) {
m_age = p.m_age;
m_Height = new int(*p.m_Height);
}
如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
构造和析构函数的调用
#include<iostream>
using namespace std;
class Person {
public:
int m_age;
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;
}
};
void test01() {
Person p1;
Person p2(30);
Person p3(p2);
cout << "p2的年龄" << p2.m_age << endl;
cout << "p3的年龄" << p3.m_age << endl;
Person p4;
Person p5 = Person(10);
Person p6 = Person(p5);
cout << "p5的年龄" << p5.m_age << endl;
cout << "p6的年龄" << p6.m_age << endl;
Person(10);
Person p7 = p6;
Person p8 = 10;
}
int main() {
test01();
system("pause");
return 0;
}
拷贝函数使用场景
1、使用一个已创建的对象来初始化一个新的对象
Person p1;
Person p3(p1);
2、值传递的方式来给函数参数传值
void doWork(Person p) {
}
void test02() {
Person p1;
doWork(p1);
}
3、以值方式返回局部对象
Person doWork2() {
Person p2;
return p2;
}
void test03() {
Person p3 = doWork2();
}
构造函数调用规则
默认情况下c++构造器至少给一个类添加3个函数
-
默认构造函数(无参,函数体为空) -
默认析构函数(无参,函数体为空) -
默认拷贝构造函数,对属性进行值拷贝
构造函数的调用规则如下:
- 如果用户定义有参构造,c++不再提供默认无参构造,但还是会提供默认拷贝构造函数
- 如果用户提供拷贝构造函数,c++不会再提供其他构造函数
初始化列表
初始化列表:用来初始化属性
#include<iostream>
using namespace std;
class Person {
public:
int age;
int height;
int weight;
Person(int a, int b, int c) :age(a), height(b), weight(c)
{
}
};
void test01() {
Person p(18, 180, 130);
cout << p.age << endl;
cout << p.height << endl;
cout << p.weight << endl;
}
int main() {
test01();
system("pause");
return 0;
}
类对象作为类成员
C++类中的成员允许是另一个类的对象,我们称该成员为:对象成员
当其他类的对象作为本类的成员:
- 在构造中,是先构造其他类的,在构造本类的。
- 在析构中,是先析构本类,再析构他类。
静态成员
静态成员变量:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
静态成员函数:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员函数
- 静态成员函数可以访问静态成员变量,不能访问非静态成员变量
静态成员访问方式:
- 通过对象进行访问
- 通过类名进行访问
3、c++对象模型和this指针
成员变量和成员函数分开存储
在c++中类内的成员变量和成员函数是分开存储的,只有非静态成员变量才属于类的对象
#include<iostream>
using namespace std;
class Person {
};
class Demo {
int m_A;
};
class Student {
void sayHello() {
}
};
class Work {
static int a;
static void sayHello() {
cout << "你好呀" << endl;
}
};
int Work::a = 10;
void test01() {
Person p;
cout << sizeof(p) << endl;
Demo d;
cout << sizeof(d) << endl;
Student s;
cout << sizeof(s) << endl;
Work w;
cout << sizeof(w) << endl;
}
int main() {
test01();
system("pause");
return 0;
}
this指针
this 是 C++中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。
#include<iostream>
using namespace std;
class Person {
public:
int age;
Person(int age) {
this->age = age;
}
Person& personAddAge(Person& person) {
this->age += person.age;
return *this;
}
Person personAddAge1(Person& person) {
this->age += person.age;
return *this;
}
};
void test01() {
Person p(18);
cout << "p的年龄为" << p.age << endl;
}
void test02() {
Person p1(10);
Person p2(10);
Person p3(10);
p2.personAddAge(p1).personAddAge(p1).personAddAge(p1);
p3.personAddAge1(p1).personAddAge1(p1).personAddAge1(p1);
cout << "p2的年龄为:" << p2.age << endl;
cout << "p3的年龄为:" << p3.age << endl;
}
int main() {
test01();
test02();
system("pause");
return 0;
}
注意事项:
- this 是 const 指针,它的值是不能被修改的,一切企图修改该指针的操作,如赋值、递增、递减等都是不允许的。
- this 只能在成员函数内部使用,用在其他地方没有意义,也是非法的。
- 只有当对象被创建后 this 才有意义,因此不能在静态成员函数中使用。
空指针访问成员函数
c++中空指针也是可以调用this指针的,但是也要注意有没有用到this指针
如果用到this指针,需要加以空指针判断保证代码的健壮性
#include<iostream>
using namespace std;
class Person {
public:
int m_Age;
void showClass() {
cout << "这是小学一年级" << endl;
}
void showAge() {
if (this == NULL) {
cout << "空指针"<< endl;
return;
}
cout << "你的年龄" << this->m_Age << endl;
}
};
void test01() {
Person* p = NULL;
p->showClass();
p->showAge();
}
int main() {
test01();
system("pause");
return 0;
}
const修饰成员函数
常函数:
- 成员函数后加上const后我们称这个函数为常函数
- 常函数内不可以修改成员属性
- 只有在成员属性声明时加上关键字mutable,才可以在常函数内进行修改
常对象:
- 声明对象前加const,称声明对象为常对象
- 常对象只能调用常函数
#include<iostream>
using namespace std;
class P {
public:
int m_A;
mutable int m_B;
void changeValue() const {
m_B = 20;
}
void func() {
}
};
void test01() {
P p;
p.changeValue();
}
void test02() {
const P p;
p.m_B = 30;
p.changeValue();
}
int main() {
test01();
test02();
system("pause");
return 0;
}
4、友元
在 C++中,一个类中可以有 public、protected、private 三种属性的成员,通过对象可以访问 public 成员,只有本类中的函数可以访问本类的 private 成员。
但是借助友元(friend),可以使得其他类中的成员函数以及全局范围内的函数访问当前类的 private 成员。
友元函数:在函数前面加 friend 关键字,这样就构成了友元函数。友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数。
友元的三种实现:
1、全局函数声明为友元函数
#include <iostream>
using namespace std;
class Building {
friend void goodFriend(Building* b);
public:
Building() {
m_LivingRoom = "客厅";
m_BedRoom = "卧室";
}
public:
string m_LivingRoom;
private:
string m_BedRoom;
};
void goodFriend(Building* b) {
cout << "好朋友正在访问" << b->m_LivingRoom << endl;
cout << "好朋友正在访问" << b->m_BedRoom << endl;
}
void test01() {
Building b;
goodFriend(&b);
}
int main() {
test01();
system("pause");
return 0;
}
2、类做友元
不仅可以将一个函数声明为一个类的“朋友”,还可以将整个类声明为另一个类的“朋友”,这就是友元类。友元类中的所有成员函数都是另外一个类的友元函数。
例如将类 B 声明为类 A 的友元类,那么类 B 中的所有成员函数都是类 A 的友元函数,可以访问类 A 的所有成员,包括 public、protected、private 属性的。
#include <iostream>
using namespace std;
class Building {
friend class GoodFriend;
public:
Building();
public:
string m_LivingRoom;
private:
string m_BedRoom;
};
class GoodFriend {
public:
GoodFriend();
void visit();
Building * b;
};
Building::Building() {
m_LivingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodFriend::GoodFriend() {
b = new Building;
}
void GoodFriend::visit() {
cout << "好朋友正在访问" << b->m_LivingRoom << endl;
cout << "好朋友正在访问" << b->m_BedRoom << endl;
}
int main() {
GoodFriend g;
g.visit();
system("pause");
return 0;
}
3、将其他类的成员函数声明为友元函数
#include <iostream>
using namespace std;
class Building;
class GoodFriend {
public:
GoodFriend();
void visit();
void visit2();
private:
Building* b;
};
class Building {
friend void GoodFriend::visit();
public:
Building();
public:
string m_LivingRoom;
private:
string m_BedRoom;
};
Building::Building() {
m_LivingRoom = "客厅";
m_BedRoom = "卧室";
}
GoodFriend::GoodFriend() {
b = new Building;
}
void GoodFriend::visit() {
cout << "1好朋友正在访问" << b->m_LivingRoom << endl;
cout << "1好朋友正在访问" << b->m_BedRoom << endl;
}
void GoodFriend::visit2() {
cout << "2好朋友正在访问" << b->m_LivingRoom << endl;
}
void test01() {
GoodFriend g;
g.visit();
g.visit2();
}
int main() {
test01();
system("pause");
return 0;
}
注意事项:
- 将 GoodFriend类声明在Building类前面,这是因为编译器从上到下编译代码,visit() 函数体中用到了 Building类的成员 m_LivingRoom和m_BedRoom,如果提前不知道 Building的具体声明内容,就不能确定 Building类是否拥有该成员(类的声明中指明了类有哪些成员)。
- 一个函数可以被多个类声明为友元函数,这样就可以访问多个类中的 private 成员。
5、运算符重载
运算符重载(Operator Overloading):同一个运算符可以有不同的功能。
通过运算符重载,扩大了C++已有运算符的功能,使之能用于对象。
加号运算符重载
把两个对象的成员分别相加,返回最终的对象
#include<iostream>
using namespace std;
class P {
public:
int m_A;
int m_B;
P() {};
P(int a, int b);
};
P::P(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
P operator+(P& p1, P& p2)
{
P temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
void test01()
{
P p1(4, 8);
P p2(6, 2);
P p3 = p1 + p2;
cout << p3.m_A << endl;
cout << p3.m_B << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
左移运算符重载
cout 是 ostream 类的对象,cin 是 istream 类的对象
#include<iostream>
using namespace std;
class P {
friend ostream& operator<< (ostream& cout, P& p);
private:
int m_A;
int m_B;
public:
P(int a, int b) {
m_A = a;
m_B = b;
}
};
ostream& operator<< (ostream& cout, P& p) {
cout << "p.m_A的值为:" << p.m_A << " p.m_B的值为:" << p.m_B;
return cout;
}
int main() {
P p(10, 20);
cout << p << " 你好呀" << endl;
system("pause");
return 0;
}
重载++和–运算符
自增++ 和自减-- 都是一元运算符,它的前置形式和后置形式都可以被重载。
#include<iostream>
using namespace std;
class MyInterger {
friend ostream& operator<<(ostream& cout, MyInterger myInt);
public:
MyInterger() {
m_Int = 0;
}
MyInterger& operator++() {
m_Int++;
return *this;
}
MyInterger operator++(int) {
MyInterger temp = *this;
m_Int++;
return temp;
}
MyInterger& operator--() {
m_Int--;
return *this;
}
MyInterger operator--(int) {
MyInterger temp = *this;
m_Int--;
return temp;
}
private:
int m_Int;
};
ostream& operator<<(ostream& cout, MyInterger myInt) {
cout << myInt.m_Int;
return cout;
}
void test01() {
MyInterger myInt;
cout << ++myInt << endl;
cout << ++myInt << endl;
cout << myInt << endl;
}
void test02() {
MyInterger myInt;
cout << myInt++ << endl;
cout << myInt++ << endl;
cout << myInt << endl;
}
void test03() {
MyInterger myInt;
cout << --myInt << endl;
cout << --(--myInt) << endl;
cout << myInt << endl;
}
void test04() {
MyInterger myInt;
cout << myInt-- << endl;
cout << (myInt--)-- << endl;
cout << myInt << endl;
}
int main() {
cout << "前置++" << endl;
test01();
cout << "后置++" << endl;
test02();
cout << "前置--" << endl;
test03();
cout << "后置--" << endl;
test04();
system("pause");
return 0;
}
赋值运算符重载
#include<iostream>
using namespace std;
class Person {
public:
int* m_Age;
Person(int age) {
m_Age = new int(age);
}
~Person() {
if (m_Age == NULL) {
return;
}
else {
delete m_Age;
m_Age = NULL;
}
}
Person& operator=(Person& p) {
if (m_Age != NULL) {
delete m_Age;
m_Age = NULL;
}
m_Age = new int(*p.m_Age);
return *this;
}
};
void test01() {
Person p(18);
Person p3(20);
Person p2(30);
p3 = p = p2;
cout << "p的年龄为:" << *p.m_Age << endl;
cout << "p3的年龄为:" << *p3.m_Age << endl;
}
int main() {
test01();
system("pause");
return 0;
}
关系运算符重载
#include <iostream>
using namespace std;
class Person {
public:
Person(string name, int age) {
m_Name = name;
m_Age = age;
}
string m_Name;
int m_Age;
bool operator==(Person& p) {
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) {
return true;
}
else {
return false;
}
}
bool operator!=(Person& p) {
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) {
return false;
}
else {
return true;
}
}
};
void test01() {
Person p1("张三", 18);
Person p2("张三", 34);
if (p1 != p2) {
cout << "p1和p2是不相等的" << endl;
}
else {
cout << "p1和p2是相等的" << endl;
}
}
int main() {
test01();
system("pause");
return 0;
}
函数调用运算符重载
- 函数调用运算符()也可以进行重载
- 由于重载后的使用方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
6、继承
一个类从另一个类获取成员变量和成员函数的过程
基本语法
class 子类 : 继承方式 父类
子类也被称为 派生类,父类也被称为 基类
class Student: public People
三种继承方式
公共继承 public(常用)
-
父类中的公共权限到子类中依旧是公共权限 -
父类中的保护权限到子类中依旧是保护权限 -
子类无法访问父类中的私有权限
保护继承 protected
-
父类中的公共权限和保护权限在子类中变成了保护权限 -
子类无法访问父类中的私有权限
私有继承 private(默认)
-
父类中的公共权限和保护权限到子类中变成了私有权限 -
子类无法访问父类中的私有权限
不同继承方式对不同属性的成员的影响结果:
protected 成员和 private 成员类似,都不能通过对象访问。但是当存在继承关系时,protected 和 private 就不一样了;父类中的 protected 成员可以在子类中使用,而父类中的 private 成员不能在子类中使用。
改变访问权限
使用 using 关键字可以改变父类成员在子类中的访问权限,例如将 public 改为 private、将 protected 改为 public。
注意:using 只能改变父类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为父类中 private 成员在子类中是不可见的,根本不能使用,所以父类中的 private 成员在子类中无论如何都不能访问。
#include<iostream>
using namespace std;
class Teacher {
public:
void show();
protected:
char m_name;
int m_age;
};
void Teacher::show() {
cout << m_name << "的年龄是" << m_age << endl;
}
class Student : public Teacher {
public:
void learning();
public:
using Teacher::m_name;
using Teacher::show;
float m_score;
private:
using Teacher::m_age;
};
void Student::learning() {
cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl;
}
int main() {
Student stu;
stu.m_name = '小明';
stu.m_age = 16;
stu.m_score = 99.5f;
stu.show();
stu.learning();
return 0;
}
继承时同名成员处理方式
遮蔽问题:如果子类中的成员(包括成员变量和成员函数)和父类中的成员重名,那么就会遮蔽从父类继承过来的成员,不管它们的参数是否一样。在子类中使用该成员(包括在定义子类时使用,也包括通过子类对象访问该成员)时,实际上使用的是子类新增的成员,而不是从父类继承来的;而且父类成员函数和子类成员函数不会构成重载。
访问静态和非静态同名成员规则:
-
访问子类同名成员,直接访问即可 -
访问父类同名成员时,需要加上作用域
# include<iostream>
using namespace std;
class Base {
public:
Base() {
m_A = 100;
}
int m_A;
void say() {
cout << "我是父类的say函数" << endl;
}
};
class Son :public Base {
public:
Son() {
m_A = 200;
}
int m_A;
void say() {
cout << "我是子类的say函数" << endl;
}
};
void test01() {
Son s;
cout << "子类的m_A变量= " << s.m_A << endl;
cout << "父类下面的m_A变量= " << s.Base::m_A << endl;
}
void test02() {
Son s;
s.say();
s.Base::say();
}
int main() {
cout << "同名的成员属性处理方式" << endl;
test01();
cout << "同名的成员函数处理方式" << endl;
test02();
system("pause");
return 0;
}
父类和子类的构造函数
类的构造函数不能被继承。因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。但是子类的构造对象可以调用父类的构造函数。
构造函数调用顺序:先调用父类,再调用子类,只能调用直接父类的构造函数,不能调用间接父类的
析构函数调用顺序:先调用子类,再调用父类
多继承
C++支持多继承(Multiple Inheritance),即一个子类可以有两个或多个父类
语法:
class 子类:继承方式 父类1,继承方式 父类2...
多继承可能导致父类中同名成员的出现,需要加作用域加以区分
c++实际开发中,不建议用多继承
菱形继承
两个派生类继承同一个基类,又有某个类同时继承这两个派生类,这种继承被称为菱形继承或者钻石继承:
如上:类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A–>B–>D 这条路径,另一份来自 A–>C–>D 这条路径。
在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B–>D 这条路径,还是来自 A–>C–>D 这条路径。
class A {
protected:
int m_a;
};
class B : public A {
protected:
int m_b;
};
class C : public A {
protected:
int m_c;
};
class D : public B, public C {
public:
void seta(int a) { m_a = a; }
void setb(int b) { m_b = b; }
void setc(int c) { m_c = c; }
void setd(int d) { m_d = d; }
private:
int m_d;
};
int main() {
D d;
return 0;
}
虚继承
虚继承解决了多继承时的命名冲突和冗余数据问题,使得在派生类中只保留一份间接基类的成员:
在继承方式前面加上 virtual 关键字就是虚继承:
class A {
protected:
int m_a;
};
class B : virtual public A {
protected:
int m_b;
};
class C : virtual public A {
protected:
int m_c;
};
class D : public B, public C {
public:
void seta(int a) { m_a = a; }
void setb(int b) { m_b = b; }
void setc(int c) { m_c = c; }
void setd(int d) { m_d = d; }
private:
int m_d;
};
int main() {
D d;
return 0;
}
这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。
7、多态
“多态(polymorphism)”指的是同一名字的事物可以完成不同的功能。
多态可以分为编译时的多态和运行时的多态。
编译时的多态(静态多态):主要是指函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定函数地址,然后确定调用哪个函数。
运行时的多态(动态多态):和继承、虚函数等概念有关,运行阶段确定函数地址。
构成动态多态的满足条件:
- 有继承关系
- 子类重写父类的虚函数
- 存在父类的指针,通过该指针调用虚函数。
动态多态使用:父类的指针或引用指向子类对象
虚函数
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
虚函数是根据指针的指向来调用的,指针指向哪个类的对象就调用哪个类的虚函数。
纯虚函数
将虚函数声明为纯虚函数,语法格式为:
virtual 返回值类型 函数名 (函数参数) = 0;
纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0 ,表明此函数为纯虚函数。
包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。
抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。
通常是基类作为抽象类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化
虚析构和纯虚析构
多态使用时,如果子类有属性开辟到堆区,那么父类指针在释放时无法调用子类的析构函数
解决方式:将父类的析构函数改为虚析构和纯虚析构
虚析构和纯虚析构异同:
同:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
异:
语法:
//虚析构
virtual ~类名(){}
//纯虚析构
virtual ~类名(){} = 0;
5、文件操作
操作文件的三大类
C++ 标准库中还专门提供了 3 个类用于实现文件操作,它们统称为文件流类,这 3 个类分别为:
- ifstream:专用于从文件中读取数据;
- ofstream:专用于向文件中写入数据;
- fstream:既可用于从文件中读取数据,又可用于向文件中写入数据。
这 3 个文件流类都位于 <fstream> 头文件中,因此在使用它们之前,程序中应先引入此头文件。
这 3 个文件流类的继承关系:
fstream类常用成员方法:
文件类型
文本文件:文件以文本的ASCII码形式存储在计算机
二进制文件:文件以文本的二进制形式存储在计算机中
打开文件
在对文件进行读写操作之前,先要打开文件。可以通过以下两种方式打开文件:
- 调用流对象的 open 成员函数打开文件。
- 定义文件流对象时,通过构造函数打开文件。
使用open函数打开文件:
void open(const char* szFileName, int mode)
下标列出各种模式标记单独使用时的作用,以及常见的两种模式标记组合的作用:
模式标记 | 适用对象 | 作用 |
---|
ios::in | ifstream fstream | 打开文件用于读取数据。如果文件不存在,则打开出错。 | ios::out | ofstream fstream | 打开文件用于写入数据。如果文件不存在,则新建该文件;如果文件原来就存在,则打开时清除原来的内容。 | ios::app | ofstream fstream | 打开文件,用于在其尾部添加数据。如果文件不存在,则新建该文件。 | ios::ate | ifstream | 打开一个已有的文件,并将文件读指针指向文件末尾(读写指 的概念后面解释)。如果文件不存在,则打开出错。 | ios:: trunc | ofstream | 打开文件时会清空内部存储的所有数据,单独使用时与 ios::out 相同。 | ios::binary | ifstream ofstream fstream | 以二进制方式打开文件。若不指定此模式,则以文本模式打开。 | ios::in | ios::out | fstream | 打开已存在的文件,既可读取其内容,也可向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。 | ios::in | ios::out | ofstream | 打开已存在的文件,可以向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。 | ios::in | ios::out | ios::trunc | fstream | 打开文件,既可读取其内容,也可向其写入数据。如果文件本来就存在,则打开时清除原来的内容;如果文件不存在,则新建该文件。 |
1、文本文件
写文件
#include<fstream>
using namespace std;
#include<iostream>
void test()
{
ofstream ofs;
ofs.open("test.txt", ios::out);
ofs << "姓名:张三" << endl;
ofs << "年龄:118" << endl;
ofs << "性别:男" << endl;
ofs.close();
}
int main()
{
test();
system("pause");
return 0;
}
读文件
#include<fstream>
using namespace std;
# include<string>
#include<iostream>
void test()
{
ifstream ifs;
ifs.open("test.txt", ios::in);
if (!ifs)
{
cout << "文件打开出错" << endl;
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){
cout << c;
}
ifs.close();
}
int main()
{
test();
system("pause");
return 0;
}
2、二进制文件
写文件
将内存中 buffer 指向的 count 个字节的内容写入文件
ostream & write(char* buffer, int count);
#include<fstream>
using namespace std;
# include<string>
#include<iostream>
class Person
{
public:
char m_name[64];
int m_age;
};
void test()
{
ofstream ofs;
ofs.open("person.txt", ios::out | ios::binary);
Person p = {"张三",18};
ofs.write((const char*)&p, sizeof(p));
ofs.close();
}
int main()
{
test();
system("pause");
return 0;
}
读文件
从文件中读取 count 个字节的数据
istream & read(char* buffer, int count);
- buffer 用于指定读取字节的起始位置,
- count 指定读取字节的个数
#include<fstream>
using namespace std;
#include<iostream>
class Person
{
public:
char m_name[64];
int m_age;
};
void test()
{
ifstream ifs;
ifs.open("person.txt", ios::out | ios::binary);
if (!ifs)
{
cout << "文件读取失败" <<endl ;
return;
}
Person p;
ifs.read((char*)&p, sizeof(p));
cout << "p的姓名" << p.m_name << "p的年龄:" << p.m_age << endl;
ifs.close();
}
int main()
{
test();
system("pause");
return 0;
}
|