重拾cpp—基础
七零八碎
内存四区
-
代码区
程序运行前划分。存放CPU执行的机器指令,其特点是共享的(可以多次打开同一个exe文件)、只读的(不能修改里面的数据)
-
全局区
程序运行前划分。存放存放全局变量、静态变量和常量(const)。该区域的数据在程序结束后由操作系统释放。
-
栈区
由编译器自动分配释放,该区存放函数的参数值,局部变量等等
注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
-
堆区
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收。在C++中主要利用new在堆区开辟内存
代码段一:
int* func()
{
int a = 10;
return &a;
}
int main()
{
int* p = func();
cout << *p << endl;
cout << *p << endl;
return 0;
}
代码段二
int* func()
{
int* a = new int(10);
return a;
}
int main()
{
int* p = func();
cout << *p << endl;
cout << *p << endl;
return 0;
}
我们来看第一段程序,第一个cout会输出10,而第二个cout输出乱码,这是因为函数返回不能是局部变量的地址。
再看第二段程序,两个cout都会输出10。
区别就在于第二个代码段中的func()函数中采用的是new开辟堆区空间,这样p能接收到func()函数的返回出来的地址
引用
相当于是变量的别名,类型& 别名 = 原名,对别名的操作实际上就是对原名进行操作
本质:是指针常量,编译器自动将&转化,例如int& a = b自动转化未int* const a = &b
int a = 10;
int& b = a;
cout << a << endl;//10
cout << b << endl;//10
b = 100;
cout << a << endl;//100
cout << b << endl;//100
注意:1、引用必须初始化,引用一但初始化后就不能更改。2、与指针类似,不要返回局部变量的引用
函数重载
作用:提高代码复用性
注意:函数返回值不能作为函数重载的条件
满足条件:
1、同一个作用域下
2、函数名相同
3、参数类型不同,或者个数不同,或者顺序不同
注意事项:
1、当写函数重载时尽量不要写默认参数如int fun(int a, int b = 10)这种写法
2、当引用作为函数参数时int fun(int& a)与int fun(const int& a)是两个不同的函数,体现了函数重载,但注意fun(10)调用的是int fun(const int& a)这个函数。编译器优化代码,相当于执行了int temp = 10; const int& a = temp;这两个代码
类和对象
C++面向对象的三大特性:封装、继承、多态
封装
-
访问权限
public:公共权限,成员类内可以访问 类外也可以访问
protected:保护权限,成员类内可以访问 类外不能访问 子类可以访问父类的保护内容
private:私有权限,成员类内可以访问 类外不能访问 子类不能访问父类的私有内容
在C++中struct默认public权限,而class默认权限是private
成员属性设置为私有
优点:1、自己控制读写权限,写一个set函数或者get函数
2、对于写权限可以检测数据的有效性比如设置年龄可在setAge函数中限制
代码样例
#include <iostream>
using namespace std;
class Person
{
private:
string m_name;
int m_age;
string m_lover = "lzl";
public:
//设置name
void setName(string name)
{
m_name = name;
}
//获取name
string getName()
{
return m_name;
}
//设置age
void setAge(int age)
{
if (age < 0 || age > 150)
{
cout << "输入年龄有误" << endl;
return;
}
m_age = age;
}
//获取情人
string getLover()
{
return m_lover;
}
};
int main()
{
Person p;
p.setName("zhangsan");
p.setAge(160);
cout << p.getName() << endl;
cout << p.getLover() << endl;
return 0;
}
-
立方体类
#include <iostream> using namespace std; class Cube { public: void setL(int l) { m_L = l; } int getL() { return m_L; } void setW(int w) { m_W = w; } int getW() { return m_W; } void setH(int h) { m_H = h; } int getH() { return m_H; } int getS() { return 2 * m_L * m_W + 2 * m_W * m_H + 2 * m_H * m_L; } int getV() { return m_L * m_H * m_W; } //类内中 bool isSameByClass(Cube c) { if (m_H == c.getH() && m_L == c.getL() && m_W == c.getW()) { return true; } else { return false; } } private: int m_L; int m_W; int m_H; }; //全局函数中 bool isSameByGlobal(Cube& c1, Cube& c2) { if (c1.getH() == c2.getH() && c1.getL() == c2.getL() && c1.getW() == c2.getW()) { return true; } else { return false; } } int main() { Cube c1; c1.setL(10); c1.setH(10); c1.setW(10); cout << c1.getS() << endl; cout << c1.getV() << endl; Cube c2; c2.setL(10); c2.setH(10); c2.setW(10); cout << isSameByGlobal(c1, c2) << endl; cout << c1.isSameByClass(c2) << endl; return 0; }
这段代码主要展示的是实现判断两个立方体是否相等的两种方法:全局方法和类内方法。不同点在函数参数的个数上是不相同的,全局方法需要两个参数,而类内方法只需要一个参数即可。
-
点与圆的位置关系
#include <iostream> using namespace std; class Point { public: void setX(int x) { m_x = x; } void setY(int y) { m_y = y; } int getX() { return m_x; } int getY() { return m_y; } private: int m_x; int m_y; }; class Circle { public: void setR(int r) { m_r = r; } int getR() { return m_r; } void setCenter(Point center) { m_Center = center; } void setCenter2(int x, int y) { m_Center.setX(x); m_Center.setX(y); } Point getCenter() { return m_Center; } private: int m_r; Point m_Center; }; void isInCircle(Circle& c, Point& p) { if ((c.getCenter().getX() - p.getX()) * (c.getCenter().getX() - p.getX()) + (c.getCenter().getY() - p.getY()) * (c.getCenter().getY() - p.getY()) == c.getR() * c.getR()) { cout << "点在圆上" << endl; } else if ((c.getCenter().getX() - p.getX()) * (c.getCenter().getX() - p.getX()) + (c.getCenter().getY() - p.getY()) * (c.getCenter().getY() - p.getY()) < c.getR() * c.getR()) { cout << "点在圆内" << endl; } else { cout << "点在圆外" << endl; } } int main() { Circle c; Point center; c.setR(10); //center.setX(10); //center.setY(0); //c.setCenter(center); c.setCenter2(10, 10); Point p; p.setX(10); p.setY(11); isInCircle(c, p); return 0; }
该案例特点主要在类中定义其他类,Circle类中定义了Point类变量,考虑到点坐标包括x和y两个变量,所以将x和y封装成一个Point类,然后Point类加上半径r构成了一个Circle类。
根据不同的成员函数(以这里的setCenter函数为例)会有不同的实现方法。这里的setCenter和setCenter2是两种不同的对圆心初始化的方法。
对象特性
构造函数
1.没有返回值也不写void
2.函数名与类名相同
3.生成对象之前自动调用,且只调用一次
4.可以有参数,所以可以发生函数重载
析构函数
1.没有返回值也不写void
2.函数名与类名相同在函数名前多一个~
以区分析构与构造
3.销毁对象之前自动调用,且只调用一次
4.不可以有参数,所以不能发生函数重载
用户不写构造和析构函数编译器会自动调用默认自带的空实现构造和析构函数
- 构造函数的分类
构造函数分为无参(默认)构造和有参构造,有参构造里有个独特的构造称为拷贝构造
#include <iostream>
using namespace std;
class Person
{
public:
Person()
{
m_age = 0;
cout << "默认构造函数" << "age = " << m_age << endl;
}
Person(int age)
{
m_age = age;
cout << "有参构造" << "age = " << m_age << endl;
}
Person(const Person& p)
{
m_age = p.m_age;
cout << "拷贝构造" << "age = " << m_age << endl;
}
int m_age;
};
void test()
{
Person p1;
Person p2(10);
Person p3(p2);
}
int main()
{
test();
return 0;
}
输出:
默认构造函数age = 0
有参构造age = 10
拷贝构造age = 10
注意拷贝构造函数的写法,以Person类为例。声明:Person(cosnt Person& p){…},调用:Person p1(10); Person p2(p1);
写默认构造函数时的注意事项:写默认构造函数一定不能打上小括号(),正确写法:Person p1; 错误写法:Person P1(); 编译器在运行的时候第二种不会报错,而是将第二种写法视为函数的声明,即返回值类型为Person,该函数名为P1且没有参数。
还有:在拷贝构造函数中一定要实现对成员属性的赋值,否则打印成员属性的时候会乱码
- 拷贝构造函数的调用时机(注意事项)
值传递的方式给函数参数传值
//类体参见上一段代码
void doWork(Person p)
{
//p.m_age = 100;
//cout << p.m_age << endl;
}
void test()
{
Person p1;
doWork(p1);
}
输出:
默认构造函数age = 0
拷贝构造age = 0
函数传参的时候可以理解为Person p = p1;这种写法等价于Person p = Person(p1)也就是Person p(p1)这种写法,所以会调用拷贝构造函数。
像Person(p1)这种称为匿名对象
- 构造函数调用规则
系统提供三个函数:
1.空实现的构造函数
2.空实现的析构函数
3.值拷贝
的拷贝构造
调用规则:
1.用户自己写了有参构造函数,系统不再提供默认构造,但仍然会提供值拷贝的拷贝构造。所以写了有参构造,无参构造就得自己写。
2.如果用户写了拷贝构造,系统就不再提供其他构造函数。所以写了拷贝构造函数,就其他构造都得自己写
- 深拷贝与浅拷贝
首先,介绍一下析构函数的作用,它是用来释放堆区开辟的数据的。这里的成员属性m_Height是一个指针,所以用析构函数来将其释放。先看下面一段代码
#include <iostream>
using namespace std;
class Person
{
public:
Person()
{
m_age = 0;
cout << "默认构造函数" << "age = " << m_age << endl;
}
Person(int age, int height)
{
m_age = age;
m_Height = new int(height);
cout << "有参构造" << endl;
}
~Person()
{
cout << "析构" << endl;
}
public:
int m_age;
int* m_Height;
};
void test()
{
Person p1(10, 160);
Person p2(p1);
}
int main()
{
test();
return 0;
}
输出:
有参构造
析构
析构
乍一看,可能不会有什么问题,是因为我们没有用到析构函数释放堆区的指针。
修改析构函数的方式如下
~Person()
{
delete m_Height;
m_Height = NULL;
cout << "析构" << endl;
}
但此时,如果你执行程序的时候会出现异常,编译器弹出中断信息。这是浅拷贝和深拷贝的区别。在上面代码中,我没有写拷贝构造函数,那么自动调用系统提供的值传递拷贝构造,这种拷贝构造就是
浅拷贝
,对m_age赋值没有问题,而问题就出在对指针m_Height赋值,这使得p1,p2各自的m_Height都指向的同一块堆区内存,导致重复释放同一块内存区,使得程序异常。接下来是解决方法,自己写一个深拷贝构造
Person(const Person& p)
{
m_age = p.m_age;
m_Height = new int(*p.m_Height);
cout << "拷贝构造" << endl;
}
m_Height = new int(*p.m_Height);这行代码需要注意的:
为什么是int呢?—>因为成员属性m_Height就是int*类型的
为什么是*p.m_Height呢?—>由于p.m_Height是一个指针,而int(),括号内是初始化的值,
一定记住是一个值!
,所以用 * 解引用取到指针p.m_Height所指的值。
-
初始化列表
#include <iostream> using namespace std; class Person { public: 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; }; void test() { Person p1(10, 160, 100); cout << p1.m_a << endl << p1.m_b << endl << p1.m_c << endl; } int main() { test(); return 0; } 输出: 10 160 100
类对象作为类成员构造函数和析构函数的调用时机:举个上面用过的例子(点与圆的位置关系)的两个类,Circle类和Point类。在Circle类中含有Point类对象center,所以在实例化Circle类对象的时候会先调用Point类的构造函数然后再是Circle类的构造函数,在函数结束之前的析构函数调用时机与构造函数调用时机恰恰相反,先调用Circle类的析构函数再调用Point类的析构函数。即
先构造的后析构
-
静态成员
-
静态变量
类内声明,类外初始化
#include <iostream> using namespace std; class Person { public: static int m_a; }; int Person::m_a = 100; void test() { Person p1; cout << p1.m_a << endl; p1.m_a = 200; cout << p1.m_a << endl; } int main() { test(); return 0; } 输出: 100 200
注意事项:
1.该案例中数据类型int不能少,作用域Person::也不能少。
2.静态成员变量不属于某个对象上,所有对象都共享同一份数据。
3.有两种访问方式:(1)通过对象访问如cout << p1.m_a;(2)通过类名访问如Person::m_a;
4.静态成员变量也有访问权限,私有的静态成员变量在类外不能访问
-
静态成员函数
1.两种访问方式,同静态成员变量
2.静态成员函数只能访问静态成员变量
3.有访问权限,私有的静态成员函数在类外不能访问
-
-
成员变量和成员函数分开存储
1.空对象占用内存空间1个字节
2.对类进行sizeof()取其大小的时候只需关注类内的非静态成员变量,静态成员(包括函数和变量)和成员函数都不属于类对象上不占用类的内存空间,所以成员变量和成员函数分开存储
-
this指针
1.this指针指向被调用的成员函数所属对象,this指针在类内默认就存在无需自己定义,如果调用的p1的构造函数this就指向p1,如果调用p2的构造函数this就指向p2
2.返回对象本身操作return *this,函数返回类型要写
Person&
这种返回引用的写法,如果是写Person的话返回的是重新拷贝出来的新数据
空指针调用成员函数
#include <iostream>
using namespace std;
class Person
{
public:
void show1()
{
cout << "hahaha\n";
}
void show2()
{
//if(this == nullptr) return;
m_a = 0;
cout << m_a << endl;
}
int m_a;
};
void test()
{
Person* p1 = nullptr;
p1->show1();
p1->show2();
}
int main()
{
test();
return 0;
}
程序输出hahaha后崩溃。show1()函数能正常运行,而show2()函数不能正常运行。在执行cout << m_a;这条语句时其实是这样的cout << this->m_a;而在实例化对象时p1指针指空导致this指针也指空,所以程序崩溃。要使程序能够正常运行则需要注释的那一行代码
- const修饰成员函数
class Person
{
public:
void show() const
{
this->m_a = 0;
this->m_b = 0;
cout << m_a << endl;
}
int m_a;
mutable int m_b;
};
1.在函数体后面加上const,那么this所指的变量就不能进行写操作,而使用mutable关键字可以防止该问题,所以只有this->m_a = 0;这一行会报错。我们称这样的函数叫做
常函数
2.对于常变量如const Person p;这时p就是一个常变量,
常变量只能调用常函数
即类中带const修饰的函数。
友元
举个例子:家中有客厅(public)和卧室(private),所有的客人都能够进去客厅,但卧室不能,也就是说只能你自己进去。还有种情况,你允许你的朋友(friend)进入你的卧室。这里在程序中也是这样体现的
- 全局函数做友元
#include <iostream>
using namespace std;
class Person
{
//全局函数show是类Person的一个好朋友,可以访问Person类内的好朋友
//friend void show(Person* p);
public:
Person()
{
this->m_a = 0;
this->m_b = 10;
}
int m_a;
private:
int m_b;
};
void show(Person* p)
{
cout << p->m_a << endl;
cout << p->m_b << endl;
}
void test()
{
Person p1;
show(&p1);
}
int main()
{
test();
return 0;
}
输出:
0
10
我们知道,全局函数是无法访问类内私有变量的,但是要是在类最前面加上friend和对全局函数的声明就能够访问私有变量了。这就是友元的作用
- 类做友元
#include <iostream>
using namespace std;
class Person
{
//说明了类GoodGay是Person类的朋友
friend class GoodGay;
public:
Person()
{
this->m_a = 0;
this->m_b = 10;
}
int m_a;
private:
int m_b;
};
class GoodGay
{
public:
GoodGay()
{
p = new Person;
}
void show()
{
cout << p->m_a << endl;
cout << p->m_b << endl;
}
Person* p;
};
void test()
{
GoodGay gg;
gg.show();
}
int main()
{
test();
return 0;
}
需要注意的点:
1.在VS环境中友元类最好写在主类后面,否则编译不能过
2.这里的写法要注意,GoodGay类的变量是Person*类型,所以在构造函数中实例化对象要用new在堆区开辟空间
- 成员函数做友元—>这里处理不好类内互相引用的问题,就不放代码了。。。
运算符重载
为什么要用到运算符重载?
在我们自定义类的时候(以Person类为例),该类中有两个成员变量m_A和m_B,假设p1和p2是Person类实例化出来的对象,那么当你执行cout << p1+p2;这段代码中就会出现”未定义运算符“的错误。
- 数字运算符重载(以加法为例)
#include <iostream>
using namespace std;
class Person
{
public:
int m_a;
int m_b;
//成员函数写法
//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;
//}
};
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;
}
//全局函数写法
Person operator+(Person& p1, int num)
{
Person temp;
temp.m_a = p1.m_a + num;
temp.m_b = p1.m_b + num;
return temp;
}
void test()
{
Person p1;
p1.m_a = 10;
p1.m_b = 20;
Person p2;
p2.m_a = 10;
p2.m_b = 20;
Person p3;
p3 = p1 + p2;
cout << p3.m_a << endl;
cout << p3.m_b << endl;
Person p4 = p3 + 10;
cout << p4.m_a << endl;
cout << p4.m_b << endl;
}
int main()
{
test();
return 0;
}
输出:
20
40
30
50
注意事项:
1.函数重载的写法(以Person类做加法为例):
类内成员函数写法—>Person operator+(Person& p){}
类外全局函数写法—>Person operator+(Person&p1 Person& p2){}
返回值类型都是Person,函数名叫operator+;如果是减就换成‘-’以此类推;传入的参数是Person类的引用即Person&,当然个人认为不传引用也没事,因为并不需要对传入的参数进行修改。
2.运算符重载函数也能进行重载,即传入的参数可以不同,所以这给程序员留下了很大的操作空间,可以自定义运算方式。上面的Person operator+(Person& p1, int num){}就使得p4+10能够计算。
- 左移运算符重载
不用成员函数重载左移运算符,因为无法使cout在左侧
#include <iostream>
using namespace std;
class Person
{
friend ostream& operator<<(ostream& out, Person& p);
public:
Person(int a, int b)
{
this->m_a = a;
this->m_b = b;
}
private:
int m_a;
int m_b;
};
//第二个参数可以以值传递的形式如Person p
ostream& operator<<(ostream& out, Person& p)
{
out << p.m_a << " " << p.m_b;
return out;
}
int main()
{
Person p(10, 20);
// cout << p;
cout << p << " hello world" << endl;
return 0;
}
注意事项:
1.左移运算符重载只能写在全局函数中,若此时类内变量为私有属性应当将其写入Person类的友元函数,这样就能使其访问私有变量
2.不同于前面的数字运算符重载,左移运算符重载书写形式为ostream& operator<<(ostream& out, Person& p)。首先参数为类Person的引用p和类ostream的引用out,然后返回值为ostream的引用,函数体内返回out,这里的out就是cout的一个别名。参数必须用ostream的引用,因为ostream对象只能有一个
3.返回ostream&的原因:由于cout是一个链式结构的形式,所以需要的返回值都必须使同一个,所以返回引用
- 递增运算符重载
#include <iostream>
using namespace std;
class myInt
{
friend ostream& operator<<(ostream& out, myInt m);
public:
myInt()
{
m_num = 0;
}
//前加
myInt& operator++()
{
this->m_num++;
return *this;
}
//后加,不能返回局部变量的引用
myInt operator++(int)//占位
{
myInt temp = *this;
this->m_num++;
return temp;
}
private:
int m_num;
};
ostream& operator<<(ostream& out, myInt m)
{
out << m.m_num;
return out;
}
int main()
{
myInt mi;
cout << ++(++mi) << endl;
cout << mi << endl;
myInt mi2;
cout << (mi2++)++ << endl;
cout << mi2 << endl;
return 0;
}
注意事项:
1.在内置数据类型中自增包括两种形式:先加型和后加型。所以我们也得实现先加和后加两种形式
2.由于用到了输出,所以我们还得写一遍<<运算符的重载,具体详情请看上面。不同的是这里的myInt是值传递。
3.自加自减运算符没有参数,所以写到成员函数里面就行。
4.对于自加的特征++(++a)这种操作是合法的,所以在重载自加运算符时应当返回类本身即myInt&,但这
仅是对于先加的情况
。在写后加的时候,由于也没有参数并且函数名和作用域相同,会导致函数重定义的情况,所以我们在写后加重载的时候会在参数中加个int,这对应的是你类中私有成员的数据类型。它表示占位以区分先加和后加两个函数。除此之外,后加的返回值为myInt类型而非引用,因为在函数体内它返回的是temp临时变量,前面说过不能返回局部变量的指针和引用!
- 赋值运算符重载
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
m_age = new int(age);
}
~Person()
{
delete m_age;
m_age = NULL;
}
Person& operator=(Person& p)
{
if (this->m_age != NULL)
{
delete this->m_age;
this->m_age = NULL;
}
this->m_age = new int(*p.m_age);
return *this;
}
int* m_age;
};
int main()
{
Person p1(10);
Person p2(20);
p2 = p1;
cout << *p1.m_age << endl << *p2.m_age << endl;
return 0;
}
补充之前所说类内系统自带的3个函数,其实还存在第四个自带的函数就是一个值传递的operator=()函数。
由于存在a = b = c这种赋值操作,所以在写operator=()重载的时候需要返回Person&,由于类内的变量是指针类型,所以传入的形参应该是引用。
在函数体内部,先判断本类变量是否有指向地址空间,如果有则将其释放掉,然后重新开辟一段空间存入*p.m_age
- 关系运算符重载(小于、大于等等)以等于为例
#include <iostream>
using namespace std;
class Person
{
friend bool operator==(Person& p1, Person& p2);
friend bool operator!=(Person& p1, Person& p2);
public:
Person(string name, int age)
{
m_name = name;
m_age = age;
}
//bool operator==(Person& p)
//{
// if (this->m_age == p.m_age && this->m_name == p.m_name)
// {
// return true;
// }
// return false;
//}
//bool operator!=(Person& p)
//{
// if (this->m_age != p.m_age || this->m_name != p.m_name)
// {
// return true;
// }
// return false;
//}
private:
string m_name;
int m_age;
};
bool operator==(Person& p1, Person& p2)
{
if (p1.m_age == p2.m_age && p1.m_name == p2.m_name)
{
return true;
}
return false;
}
bool operator!=(Person& p1, Person& p2)
{
if (p1.m_age != p2.m_age || p1.m_name != p2.m_name)
{
return true;
}
return false;
}
int main()
{
Person p1("tom", 11);
Person p2("to", 11);
if (p1 == p2)
{
cout << "相等" << endl;
}
else
{
cout << "不相等" << endl;
}
if (p1 != p2)
{
cout << "不相等" << endl;
}
else
{
cout << "相等" << endl;
}
return 0;
}
- 函数调用运算符重载(又称仿函数,在后续STL会常用到)
#include <iostream>
using namespace std;
class MyPrint
{
public:
void operator()(string str)
{
cout << str << endl;
}
};
class MyAdd
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
int main()
{
MyPrint myprint;
myprint("lalala");
MyPrint()("hahaha");
MyAdd myadd;
cout << myadd(10, 20) << endl;
cout << MyAdd()(10, 30) << endl;
return 0;
}
仿函数有不同的写法,可以是参数类型不同、个数不同、返回值类型不同。
void operator()(string str)中operator()是重载函数名,string str是传入的参数
有两种方式的调用:
1.先实例化对象,再使用
对象()
的形式调用2.使用匿名函数对象,如
类名()(...)
,类名()就称为匿名对象,加上仿函数的调用就称为匿名函数对象。
继承
优点:减少重复的代码
语法:
class 子类 : 继承方式 父类
派生类中的成员包含两大部分
一类是从基类继承过来的,一类是自己增加的成员
从基类继承过来的表现其共性,而新增的成员体现了其个性
继承方式有三种
- 公共继承—public
- 保护继承—protected
- 私有继承—private
牢记该图片
继承中的构造和析构顺序
先构造父类,再构造子类,析构的顺序与构造的顺序相反
- 继承中同名成员的处理
#include <iostream>
using namespace std;
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 = 200;
}
void func()
{
cout << "Son下的func()" << endl;
}
int m_a;
};
int main()
{
Son s;
cout << s.m_a << endl;
cout << s.Base::m_a << endl;
s.func();
s.Base::func();
s.Base::func(100);
return 0;
}
在继承中,子类的同名变量和函数会隐藏掉父类的所有同名变量和函数,包括父类重载过的同名函数。所以在我们实例化了子类对象之后如何访问父类的成员呢?
该场景是以类内访问权限和继承方式都是public的情况下:首先如果访问的是子类的变量和函数,我们只需要用s.m_a和s.func()的形式就行;如果是访问父类的变量和函数,我们就需要加上其父类的作用域。如s.Base::m_a和s.Base::func()
继承中的同名静态成员处理方式与其相同
对于多继承,如果父类中出现了同名情况,子类使用的时候要加作用域
- 菱形继承
#include <iostream>
using namespace std;
class Animal
{
public:
int m_a;
};
class Sheep : virtual public Animal{};
class Tuo : virtual public Animal{};
class SheepTuo : public Sheep, public Tuo{};
int main()
{
SheepTuo st;
st.Sheep::m_a = 18;
st.Tuo::m_a = 28;
cout << st.Sheep::m_a << endl;
cout << st.Tuo::m_a << endl;
return 0;
}
菱形继承类似于多继承,在实际的应用中很少。当父类变量继承下来之后会存在两个同名的变量。在代码中如果不加virtual关键字的话输出的会是18和28,在加入了virtual关键字之后就使普通继承转变成了虚继承,这样在对其子类同名变量进行读写的时候是用到的同一份数据,所以两次输出结果都是28.
多态
多态分为两类
- 静态多态:函数重载、运算符重载。复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定—编译阶段确定函数地址
- 动态多态的函数地址晚绑定—运行阶段确定函数地址
动态多态满足条件:
1.有继承关系
2.子类重写父类的虚函数
区分重写和重载:
重写:函数名、返回值类型、参数列表都完全相同
重载:函数名相同,但参数类型、顺序、个数可能不同。
返回值类型
不能作为重载的条件
动态多态的使用:父类的指针或引用指向子类的对象
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void speak()
{
cout << "动物" << endl;
}
};
class Cat : public Animal
{
public:
void speak()
{
cout << "猫" << endl;
}
};
class Dog : public Animal
{
public:
void speak()
{
cout << "狗" << endl;
}
};
void doSpeak(Animal& a)
{
a.speak();
}
int main()
{
Cat c;
doSpeak(c);
Dog d;
doSpeak(d);
return 0;
}
doSpeak(Animal& a)函数调用的时候使用了动态多态,在参数传进来的时候等价于Animal& a = c;即父类引用指向子类对象,所以此时的doSpeak函数就会分别输出猫和狗。但是当父类中的speak函数去掉了virtual关键字的时候,两次都是输出的动物。所以动态多态的两种条件就是有继承关系和子类重写父类的虚函数。虚函数就指的是带virtual关键字的函数
多态的好处:
1.组织结构很清晰
2.可读性强
3.对于前期和后期扩展以及维护性高
计算器类
#include <iostream>
using namespace std;
class AbstractCalculation
{
public:
virtual int getResult()
{
return 0;
}
int a;
int b;
};
class addCalculation : public AbstractCalculation
{
public:
int getResult()
{
return a + b;
}
};
class subCalculation : public AbstractCalculation
{
public:
int getResult()
{
return a - b;
}
};
class mulCalculation : public AbstractCalculation
{
public:
int getResult()
{
return a * b;
}
};
int main()
{
AbstractCalculation* ac = new addCalculation;
ac->a = 100;
ac->b = 10;
cout << ac->getResult() << endl;
delete ac;
ac = new subCalculation;
ac->a = 100;
ac->b = 10;
cout << ac->getResult() << endl;
delete ac;
ac = new mulCalculation;
ac->a = 100;
ac->b = 10;
cout << ac->getResult() << endl;
return 0;
}
- 纯虚函数和抽象类
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类就称为抽象类
抽象类特点
1.无法实例化对象
2.子类必须重写抽象类中的纯虚函数,否则也属于抽象类
制作饮品
#include <iostream>
using namespace std;
class AbstractDrinking
{
public:
//煮水
virtual void Boil() = 0;
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void PutSomething() = 0;
//制作饮品
void makeDrink()
{
Boil();
Brew();
PourInCup();
PutSomething();
}
};
class Coffee : public AbstractDrinking
{
//煮水
virtual void Boil()
{
cout << "煮农夫山泉" << endl;
}
//冲泡
virtual void Brew()
{
cout << "冲泡咖啡" << endl;
}
//倒入杯中
virtual void PourInCup()
{
cout << "倒入杯中" << endl;
}
//加入辅料
virtual void PutSomething()
{
cout << "加入糖和牛奶" << endl;
}
};
class Tea : public AbstractDrinking
{
//煮水
virtual void Boil()
{
cout << "煮矿泉水" << endl;
}
//冲泡
virtual void Brew()
{
cout << "冲泡茶叶" << endl;
}
//倒入杯中
virtual void PourInCup()
{
cout << "倒入杯中" << endl;
}
//加入辅料
virtual void PutSomething()
{
cout << "加入枸杞" << endl;
}
};
void doWork(AbstractDrinking* abs)
{
abs->makeDrink();
delete abs;
}
int main()
{
doWork(new Tea);
cout << "------------------" << endl;
doWork(new Coffee);
return 0;
}
- 虚析构和纯虚析构
1.虚析构或纯虚析构是用来解决通过父类指针释放子类对象的
2.如果子类中没有堆区数据,可以不写虚析构或纯虚析构
3.拥有纯虚析构的类也属于抽象类,不能实例化对象
#include <iostream>
using namespace std;
class Animal
{
public:
Animal()
{
cout << "animal's gouzao" << endl;
}
virtual void speak()
{
cout << "animal is speaking" << endl;
}
//virtual ~Animal()
//{
// cout << "animal's chunxigou" << endl;
//}
virtual ~Animal() = 0;
};
Animal::~Animal()
{
cout << "animal's chunxuxigou" << endl;
}
class Cat : public Animal
{
public:
Cat(string name)
{
cout << "cat's gouzao" << endl;
m_Name = new string(name);
}
virtual void speak()
{
cout << *m_Name << " is speaking" << endl;
}
~Cat()
{
cout << "cat's xigou" << endl;
}
string* m_Name;
};
int main()
{
Animal* a = new Cat("hahaha");
a->speak();
delete a;
return 0;
}
纯虚析构只能在类内声明在类外实现。在类内写virtual~类名() = 0;在类外类名::~类名(){…}
虚析构则可以在类内实现
电脑组装案例
#include <iostream>
using namespace std;
//抽象CPU类
class CPU
{
public:
virtual void calculate() = 0;
};
//抽象显卡类
class VideoCard
{
public:
//抽象的显示函数
virtual void display() = 0;
};
//抽象内存条
class Memory
{
public:
virtual void storage() = 0;
};
class Computer
{
public:
Computer(CPU* cpu, VideoCard* vc, Memory* mem)
{
m_cpu = cpu;
m_vc = vc;
m_mem = mem;
}
//提供工作的函数
void work()
{
m_cpu->calculate();
m_vc->display();
m_mem->storage();
}
~Computer()
{
if (m_cpu != NULL)
{
delete m_cpu;
m_cpu = NULL;
}
if (m_vc != NULL)
{
delete m_vc;
m_vc = NULL;
}
if (m_mem != NULL)
{
delete m_mem;
m_mem = NULL;
}
}
private:
CPU* m_cpu;
VideoCard* m_vc;
Memory* m_mem;
};
//具体厂商
//Intel
class IntelCPU : public CPU
{
virtual void calculate()
{
cout << "Intel的CPU开始计算了" << endl;
}
};
class IntelVideoCard : public VideoCard
{
public:
virtual void display()
{
cout << "Intel的显卡开始显示了" << endl;
}
};
class IntelMemory : public Memory
{
public:
virtual void storage()
{
cout << "Intel的内存条开始存储了" << endl;
}
};
//Lenovo
class LenovoCPU : public CPU
{
virtual void calculate()
{
cout << "Lenovo的CPU开始计算了" << endl;
}
};
class LenovoVideoCard : public VideoCard
{
public:
virtual void display()
{
cout << "Lenovo的显卡开始显示了" << endl;
}
};
class LenovoMemory : public Memory
{
public:
virtual void storage()
{
cout << "Lenovo的内存条开始存储了" << endl;
}
};
int main()
{
//第一台Intel
CPU* intel = new IntelCPU;
VideoCard* intelCard = new IntelVideoCard;
Memory* intelMem = new IntelMemory;
Computer* c1 = new Computer(intel, intelCard, intelMem);
c1->work();
delete c1;
cout << "---------------------" << endl;
//第二台Lenovo
Computer* c2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);
c2->work();
delete c2;
cout << "---------------------" << endl;
//第三台
Computer* c3 = new Computer(new IntelCPU, new LenovoVideoCard, new IntelMemory);
c3->work();
delete c3;
return 0;
}
文件操作
C++中对文件操作需要包含头文件fstream
写文件步骤
1.包含头文件-----#include < fstream >
2.创建流对象-----ofstream ofs;
3.打开文件------ofs.open(“文件路径”, 打开方式);
4.写数据-------ofs << “写入的数据”;
5.关闭文件------ofs.close();
文件的打开方式:
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
ofstream ofs;
ofs.open("test.txt", ios::out);
ofs << "lzl hao shuai" << endl;
ofs << "lzl hao shuai" << endl;
ofs << "lzl hao shuai" << endl;
ofs << "lzl hao shuai" << endl;
ofs << "lzl hao shuai" << endl;
ofs.close();
return 0;
}
严格按照步骤进行,创建流对象的时候切记ofstream—只写、istream—只读、ostream—可读可写
open()函数内两个参数,第一个参数为要打开的文件路径,第二个参数为打开方式,常见的打开方式见上图
在输入文件路径的时候,如果查询不到,那么会在cpp文件的同级目录下创建一个该文件
哦对,操作完毕不能忘了关闭文件,就像malloc后要free,new后要delete
读文件步骤
1.包含头文件-----#include < fstream >
2.创建流对象-----ifstream ifs;
3.打开文件------ifs.open(“文件路径”, 打开方式);并且判断是否打开成功
----isopen()函数
4.四种方式读取
5.关闭文件------ofs.close();
二进制文件的写
#include <iostream>
#include <fstream>
using namespace std;
class Person
{
public:
Person(string name, int age)
{
m_name = name;
m_age = age;
}
string m_name;
int m_age;
};
int main()
{
//构造函数写法 实例化对象之后直接选择文件并打开
ofstream ofs("person.txt", ios::out | ios::binary | ios::app);
Person p{ "wangwu", 29 };
ofs.write((const char*)&p, sizeof(Person));
ofs.close();
return 0;
}
ofs.write()函数有两个参数,第一个参数为const char类型的地址需要强制转换后加&,第二个类型为Person类的大小
打开时还需要ios::binary,用 | 连接
二进制的方式读
#include <iostream>
#include <fstream>
using namespace std;
class Person
{
public:
char m_name[64];
int m_age;
};
int main()
{
ifstream ifs("person.txt", ios::in | ios::binary);
if (!ifs.is_open())
{
cout << "打开失败" << endl;
return 0;
}
Person p;
ifs.read((char*)&p, sizeof(Person));
cout << p.m_name << " " << p.m_age << endl;
return 0;
}
无论是文本文件的读还是二进制文件的读,在打开文件收都需要有判断文件是否打开操作,若文件没有打开则需要退出程序
二进制写操作主要用read()函数,第一个参数为char*的地址需要强制类型转换后加&,第二个参数为Person类的大小