一、C++基础入门
1、变量
#include<iostream>
using namespace std;
int main()
{
/*
变量创建的语法:
数据类型 变量名 = 变量初始值;
*/
int a = 18;
cout << "C++的变量类型,a = " << a << endl;
system("pause");
return 0;
}
2、常量
1、#define定义宏常量
#define Week 7
2、const 修饰
const int a = 23;
3、关键字
4、标识符命名规则
C++标识符(变量、常量)命名规则
- 标识符不能是关键字
- 标识符只能由字母、数字、下划线组成
- 第一个字符必须为字母或下划线
- 标识符中字母区分大小写
5、C++输入
cin<< 输入
#include<iostream>
using namespace std;
int main()
{
// cin>> :从键盘输入数据
int a = 0;
cout << "请输入a的值" << endl;
cin >> a;
cout << "a = " << a << endl;
//输入字符串
string str;
cout << "请输入字符串的值" << endl;
cin >> str;
cout << "str = " << str << endl;
//接受键盘输入的字符型数据
float f1;
cout << "请输入浮点型数字" << endl;
cin >> f1;
cout << "f1 = " << f1 << endl;
system("pause");
return 0;
}
二、指针
1、const修饰指针
const修饰指针有三种情况:
- const修饰指针 — 常量指针
int a = 10;
int b = 10;
const int *p = &a; //在指针前面加const修饰
p = &b; //正确,指向可以修改
*p = 20; //错误,const修饰的是指针,所以取*的操作不允许。指针指向的值不可以修改
常量指针特点:指针的指向可以修改,但指针指向的值不可以更改。
- const修饰常量 — 指针常量
int a = 10;
int b = 10;
int * const p = &a; //指针常量(const修饰p,前面int*表示是一个指针)
p = &b;//const修饰变量,所以p不能被修改。错误,指针的指向不可以修改
*p = 20; //正确
指针常量特点:指针的指向不可以修改,但指针指向的值可以更改。
- const既修饰指针、又修饰量
int a = 10;
int b = 10;
const int* const p = &a; //const既修饰指针、又修饰常量
特点:指针的指向和指针指向的值都不可以修改。
三、内存结构
1、C语言运行之前
1、预处理:宏定义展开、头文件展开、条件编译,不会检查语法
2、编译:检查语法,将预处理文件编译生成汇编文件
3、汇编:将汇编文件生成目标文件(二进制文件)
4、链接:将目标文件链接为可执行程序
程序编译之后,生成了exe可执行程序,未执行该程序之前分为两个区域:
-
**代码区:**存放CPU执行的机器指令、共享的、只读的
-
全局区:全局变量、静态变量;常量区(包括字符串常量和其他常量)
总结:
- C/C++在程序运行之前分为全局区和代码区
- 代码区的特点是共享和只读
- 全局区中存放**全局变量、静态变量、常量**
- 常量区中存放const修饰的全局变量和字符串常量
2、运行之后
2.1、栈区
- 由编译器自动分配释放、存放函数的参数值、局部变量等
- 注意:不要返回局部变量的地址。因为栈区开辟的数据由编译器自动释放
int fun(int b){ //形参数据也放在栈区
int a = 10;
return &a; //返回局部变量的地址
}
void main()
{
int b = 4;
int *p = fun(b);
cout<<*p<<endl; //打印10,正确。这是因为编译器做了保留
cout<<*p<<endl; //打印输出错误,第二次这个数据就被销毁了。
}
2.2、堆区
- 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
- 手动释放利用操作符delete
- 在C++中,主要利用new在堆区开辟
**类型* | **作用域* | **生命周期* | **存储位置* |
---|---|---|---|
auto变量 | 一对{}内 | 当前函数 | 栈区 |
static局部变量 | 一对{}内 | 整个程序运行期 | 初始化在data段,未初始化则在BSS段 |
extern变量 | 整个程序 | 整个程序运行期 | 初始化在data段,未初始化则在BSS段 |
static全局变量 | 当前文件 | 整个程序运行期 | 初始化在data段,未初始化则在BSS段 |
extern函数 | 整个程序 | 整个程序运行期 | 代码区 |
static函数 | 当前文件 | 整个程序运行期 | 代码区 |
register变量 | 一对{}内 | 当前函数 | 运行时存储在CPU寄存器 |
字符串常量 | 当前文件 | 整个程序运行期 | data段 |
3、new关键字
#include<iostream>
using namespace std;
//在堆上生成数据
int* fun1()
{
int* p = new int(10);
return p;
}
//2 在堆上利用new 开辟数组
void test02()
{
//创建数组
int * arr = new int[10];
for (int i = 0; i < 10; i++)
{
arr[i] = i + 1;
}
for (int i = 0; i < 10; i++)
{
cout << arr[i]<<", ";
}
//释放数组,需要加一个中括号 " [] "
delete[] arr;
}
void main()
{
int* p = fun1();
cout << *p << endl;
cout << *p << endl;
cout << *p << endl;
delete p; //清除堆上面的内存
test02();
}
4、易混淆概念
代码区:存放程序编译后的二进制代码,不可寻址区
数据区包括:堆,栈,全局/静态存储区
全局/静态存储区包括:全局区(extent),静态区(static),常量区(const)
常量区包括:字符串常量、常变量(const)
5、引用
给变量起别名
1、语法格式:
数据类型 &别名 = 原名
2、引用注意点
- 引用必须要初始化; int &b; //错误,没有初始化
- 引用一旦初始化之后,就不可以更改了
3、引用做函数返回值
- 1、不要返回局部变量的引用
- 2、函数的调用可以作为左值
#include<iostream>
using namespace std;
//引用做函数的返回值
//1、不要返回局部变量的引用
int& test01()
{
int a = 10; //局部变量在栈区'
return a; //返回局部变量
}
//2、函数的调用可以作为左值
int& test02() {
static int a = 10; //静态变量,存放在全局区。全局区的数据在程序结束后系统释放
return a;
}
int main()
{
int& ref = test01();
cout << "ref = " << ref << endl; //第一次时打印正确,因为编译器做了保存
cout << "ref = " << ref << endl; //第二次打印错误
int& ref2 = test02();
cout << "\nref2 = " << ref2 << endl;
test02() = 100; //相当于:a = 100; 如果函数的返回值是引用,那么这个函数调用可以作为左值
cout << "\nref2 = " << ref2 << endl;
system("pause");
return 0;
}
4、引用的本质
引用本质在C++内部实现是一个指针常量
- 指针常量(const修饰变量名):指针的指向不可以改变,指针指向的值可以改变
#include<iostream>
using namespace std;
int main()
{
//引用的本质是一个指针常量
int a = 10;
//自动转换为 int* const ref = &a; 指针常量:指针的指向不可以改变,但指向的值可以改变
int& ref = a;
ref = 20; //内部发现是一个引用,则自动转换为:*ref = 20;
cout << "a = " << a << endl;
cout << "ref = " << ref << endl; //自动转换为 *ref
system("pause");
return 0;
}
指针常量练习:
#include<iostream>
using namespace std;
int main()
{
//引用的本质是一个指针常量
int a = 10;
int b = 20;
int* const p = &a; //指针常量:指针的指向不可以改变,但是指针指向的值可以改变
//p = &b; //非法,指针常量的指向不可以改变(const修饰哪一个,哪一个就不可以改变)
*p = b; //指针指向的值可以改变
cout << "*p = " << *p <<", a = "<< a << endl;
system("pause");
return 0;
}
5、常量引用
作用:主要用来修饰形参,防止误操作。
#include<iostream>
using namespace std;
void print(const int & value)
{
//value = -1; //加入const之后只读,不能修改
cout << "value = " << value << endl;
}
int main()
{
int a = 1000;
int& ref = a;
//引用必须引一块合法的内存空间
//const int& ref2 = 10;实际上编译器做了如下操作:int temp = 10; const int& ref2 = temp;
const int& ref2 = 10; //加入const之后只读,不能修改
cout << "ref2 = " << ref2 << endl;
print(a);
cout << "a = " << a << endl;
system("pause");
return 0;
}
三、函数提高
1、形参默认值、占位符
void fun(int a,int b = 10, int c = 20){ //默认值
}
void fun(int a,int){ //第二个参数为占位参数,调用此函数时必须赋值
}
2、函数重载
2.1、概述
- 函数名相同,提高复用性
重载条件:
- 同一个作用域下
- 函数名称相同
- 函数参数类型不同,或者个数不同、顺序不同
注意:函数返回值不可以作为函数重载的条件
2.2、函数重载注意事项
- 引用作为重载条件
#include<iostream>
using namespace std;
void func(int& a)
{
cout << "int& a引用参数函数" << endl;
}
void func(const int& a) {
cout << "const int& a引用作为函数参数" << endl;
}
int main02()
{
int a = 10;
func(a); //传入参数是变量,所以调用func(int& a)。输出 int& a引用参数函数
const int b = 10;
func(b); //传入常量,所以调用func(const int& a)。输出: const int& a引用作为函数参数
system("pause");
return 0;
}
- 函数重载碰到函数默认值:注意出现二义性
四、类和对象
4.1 封装
4.1.1 封装的意义
意义一:
意义二:类在设计时,可以把属性和行为放在不同的权限下,加以控制。
访问权限:
public:任何地方都可以访问(类内和类外)
protected:类外不可以访问
private:类外不可以访问
4.1.2 struct和class的区别
struct默认权限为公共public;class默认权限为私有 private
4.1.3 成员属性私有化
优点1:将所有成员属性设置为私有,可以控制读写权限
优点2:对于写权限,可以检测数据的有效性
案例
#include<iostream>
using namespace std;
//定义一个立方体类
class Cube{
private:
float m_L; //长
float m_H; //高
float m_W; //宽
public:
//get set方法
void setCube(float L, float W, float H){
m_L = L;
m_W = W;
m_H = H;
}
float* getCube(){
float cubeSide[] = {m_L,m_W,m_H};
return cubeSide;
}
//计算边长
float calculateSide(){
float res = (m_L + m_W + m_H) * 2;
return res;
}
float calculateArea(){
float area = m_L * m_W * m_H;
return area;
}
};
int main()
{
Cube cube1;
//设置第一个立方体的边长
cube1.setCube(2.4,3,4);
Cube cube2;
cube2.setCube(3,5,1.6);
cout<<"cube1的周长为:"<<cube1.calculateSide()<<endl;
cout<<"cube1的面积为:"<<cube1.calculateArea()<<endl;
cout<<"cube2的周长为:"<<cube2.calculateSide()<<endl;
cout<<"cube2的面积为:"<<cube2.calculateArea()<<endl;
system("pause");
return 0;
}
4.2 对象的初始化和清理
4.2.1 构造函数和析构函数
- 构造函数:主要作用于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用
- 析构函数:主要作用于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法: 类名(){}
- 构造函数,没有返回值也不写void
- 函数名与类名相同
- 可以有参数,因此可以发生重载
- 自动调用,而且只会调用一次
析构函数语法:~类名(){}
- 没有返回值也不写void
- 函数名与类名相同,在名称前加上符号~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前自动调用析构函数,无须手动调用,而且只调用一次
#include<iostream>
using namespace std;
class Person{
public:
Person(){
cout<<"构造函数调用"<<endl;
}
//析构函数
~Person(){ //自动调用,不能有参数
cout<<"析构函数调用"<<endl;
}
};
void test01(){
Person p1; //创建对象
}
int main()
{
test01();
system("pause");
return 0;
}
4.2.2 构造函数的分类及调用
-
按参数分为:有参构造、无参构造
-
按类型分为:普通构造、拷贝构造
//拷贝构造函数
Person(const Person &p){
//将传入的参数身上的所有属性,都拷贝到自己身上
age = p.age;
}
- 按调用方式分:括号法、显示法、隐士转换法
#include<iostream>
using namespace std;
class Person{
public:
Person(){
cout<<"无参构造函数调用"<<endl;
}
Person(int a){
age = a;
cout<<"有参构造函数调用"<<endl;
}
~Person(){ //自动调用
cout<<"析构函数调用"<<endl;
}
//拷贝构造函数
Person(const Person &p){
//将传入的人身上的所有属性,都拷贝到我身上
cout<<"拷贝构造函数调用"<<endl;
age = p.age;
}
private:
int age;
};
void test01(){
//注意事项1:使用默认的无参构造时,不能加().因为会认为是一个函数声明
Person p1; //创建对象
//按调用分类:括号法、显示法、隐士转换法
//不能使用括号法调用无参构造
//Person p2(10);
//2、显示法
Person p3 = Person(p1);
Person(20); //匿名对象。当前执行完成,立即被回收(调用析构函数)
cout<<"aaa"<<endl;
//注意事项2:不要利用拷贝构造函数,初始化匿名对象。
//编译器会认为 Person(p3) == Person p3; //重定义错误
//3、隐士转换法
Person p4 = 10; //相当于 Person p4 = Person(10);
Person p5 = p4; //拷贝构造函数
}
int main()
{
test01();
system("pause");
return 0;
}
注意事项1:使用默认的无参构造时,不能加(). 因为会认为是一个函数声明
注意事项2:不要利用拷贝构造函数,初始化匿名对象。
4.2.3 拷贝构造函数调用时机
1、使用一个已经创建完毕的对象来初始化一个新对象
2、值传递的方式给函数参数传值
void doWork(Person p1){
//值传递的方式给函数参数传值,会调用拷贝构造函数复制一份相同的对象,在此函数中对p1进行修改,不会影响调用函数中的实参对象
}
3、值的方式返回局部对象
Person doWork2(){
Person p1;
return p1; //返回局部对象会调用拷贝构造函数复制出一个相同对象,原始的p1在栈中,函数结束时就会被销毁返回
}
4.2.4 构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
- 1、默认构造函数(无参、函数体为空)
- 2、默认析构函数(无参、函数体为空)
- 3、默认拷贝构造函数,对属性进行值拷贝
调用规则:
- 如果用户定义了有参构造函数,C++不再提供默认无参构造函数,但是会提供默认拷贝构造函数
- 如果用户定义了拷贝构造函数,C++不再提供其他构造函数
4.2.5 深拷贝和浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
#include<iostream>
using namespace std;
class Person {
public:
Person() {
cout << "无参构造函数" << endl;
}
Person(int a,float height) {
m_Age = a;
m_Hight = new float(height);
cout << "有参构造函数" << endl;
}
~Person() {
//析构函数,将堆区开辟的数据做释放操作
if (m_Hight != NULL) {
delete m_Hight;
m_Hight = NULL;
}
cout << "析构函数" << endl;
}
Person(const Person& p) {
cout << "拷贝构造函数" << endl;
m_Age = p.m_Age; //浅拷贝
//浅拷贝 m_Hight = p.m_Hight; 在堆区的数据存在重复释放内存的问题
//这里进行深拷贝操作,解决上述问题
m_Hight = new float(*p.m_Hight);
}
int m_Age;
float* m_Hight; //身高指针
};
void test01()
{
Person p1(27, 1.67);
cout << "p1的年龄为: " <<p1.m_Age <<" 身高为:"<<*p1.m_Hight<< endl;
Person p2(p1);
cout << "p2的年龄为: " << p2.m_Age << " 身高为:" << *p2.m_Hight << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
总结:如果属性有在堆区开辟的,一定要程序员提供拷贝构造函数,防止浅拷贝带来的问题。
4.2.6 初始化列表
作用:C++提供了初始化列表语法,初始化属性
语法:构造函数(): 属性1(值1), 属性2(值2),…{}
class Person {
public:
//初始化列表
Person(int a, int b, int c) : m_A(a), m_C(c), m_B(b) {}
int m_A, m_B, m_C;
};
4.2.7 类对象作为类成员
构造顺序:先构造成员对象,在构造当前类。析构顺序与构造相反
4.2.8 静态成员
静态成员就是在成员变量和成员函数前加上关键字static。
分类:
- 静态成员变量
- 所有对象共享一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
#include<iostream>
using namespace std;
class Person {
public:
static void func() {
m_Age = 10;
//m_Hight = 178; 静态成员函数不能访问非静态成员变量。因为无法区分该变量是属于谁的
cout << "static void func调用" << endl;
}
static int m_Age;
int m_Hight;
};
void test04_01()
{
//1、通过对象访问
Person p;
p.func();
//2、通过类名直接访问
Person::func();
}
int main()
{
test04_01();
system("pause");
return 0;
}
4.3 C++对象模型和this指针
4.3.1 成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上 : 空对象占用内存空间为:1
#include<iostream>
using namespace std;
class Person {
int m_A; //非静态成员变量,属于类的对象上的
static int m_B; //静态成员变量,不属于类的对象上
void func() //非静态成员函数,不属于类的对象上
{
}
static void func2()//静态成员函数,不属于类的对象上
{
}
};
int Person::m_B = 1; //静态成员变量,需要在类外赋初值
void test01() {
Person p;
//空对象占用内存空间为:1
//C++编译器会给每个空对象分配一个字节空间,为了区分空对象占内存的位置。
//每个空对象都有一个独一无二的内存地址
cout << "空对象所在字节数为:" << sizeof(p) << endl;
}
void test02() {
Person p;
cout << "sizeof(p) = " << sizeof(p) << endl;
}
int main() {
test02();
system("pause");
return 0;
}
4.3.2 this指针
C++中成员变量和成员函数是分开存储的,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。this指针用于区分对象调用自己的代码。
this指针本质上是一个指针常量:指针的指向不可以修改
this指针指向被调用的成员函数所属的对象
this指针隐含每一个非静态成员函数内的一种指针
this指针的用途:
- 当形参和成员变量同名时,可以用this指针来区分
Person(int m_A) {
//this指向被调用的成员函数所属的对象
this->m_A = m_A;
}
- 在类的非静态成员函数中返回对象本身,可使用return *this.
Person& addPersonAge(Person &p) { //返回的是一个引用,否则(拷贝构造函数)将采用浅拷贝方式返回一个新的对象
this->m_A += p.m_A;
return *this; //*this是Person
}
4.3.3 空指针访问成员函数
空指针也可以调用成员函数,但是要注意有没有用到this指针。如果用到this指针,需要加以判断保证代码的健壮性。
4.3.4 const修饰成员函数
常函数
- 在成员函数后面加const修饰的函数称为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
class Person {
public:
/*
this指针的本质是 指针常量 ,所以this指针的指向不可以修改
const Person * const this
在成员函数后面加const,修饰的是this指向,让指针指向的值也不可以修改
*/
void showPerson() const
{
// this->age = 10; 不可以被修改
this->height = 170;
}
int age;
mutable int height; //特殊变量,mutable修饰的变量可以被常函数修改
};
常对象
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
#include<iostream>
using namespace std;
class Person {
public:
/*
this指针的本质是 指针常量 ,所以this指针的指向不可以修改
const Person * const this
在成员函数后面加const,修饰的是this指向,让指针指向的值也不可以修改
*/
void showPerson() const
{
// this->age = 10; 不可以被修改
this->height = 170;
}
int age;
mutable int height; //特殊变量,mutable修饰的变量可以被常函数修改
};
int main()
{
const Person p;
//p.age = 10; //不能修改
p.height = 189; //常对象可以修改mutable修饰的变量
p.showPerson(); //常对象只能调用常函数
system("pause");
return 0;
}
4.4 友元
友元的关键字:friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
4.4.1 全局函数做友元
class Building
{
friend void test01_01(); //声明该全局函数为友元
public:
Building() {
m_sittingRoot = "客厅";
m_bedRoot = "卧室";
}
string m_sittingRoot;//客厅
private:
string m_bedRoot; //卧室
};
//全局函数
void test01_01() {
Building bu;
bu.m_sittingRoot;
cout << "友元全局函数正在访问:" << bu.m_sittingRoot << endl;
cout << "友元全局函数正在访问:" << bu.m_bedRoot<< endl;
}
4.4.2 类做友元
class Building
{
friend class goodGay; //声明类goodGay为友元
public:
Building() {
m_sittingRoot = "客厅";
m_bedRoot = "卧室";
}
string m_sittingRoot;//客厅
private:
string m_bedRoot; //卧室
};
class goodGay{
}
4.4.3 成员函数做友元
class Building
{
friend void goodGay::visit(); //声明成员函数visit为友元
public:
Building() {
m_sittingRoot = "客厅";
m_bedRoot = "卧室";
}
string m_sittingRoot;//客厅
private:
string m_bedRoot; //卧室
};
class goodGay{
public:
void visit(){
cout<<"好基友正在访问"<<endl;
}
}
4.5 运算符重载
运算符重载有两种方式:1)成员函数重载; 2)全局函数重载
4.5.1 加号重载
#include<iostream>
using namespace std;
class Person
{
friend Person operator+(Person& p1, Person& p2); //友元函数
friend ostream& operator<<(ostream& cout, Person& p); //友元函数
public:
Person() {
m_A = 10;
m_B = 20;
}
//2. 加号的成员函数重载
Person operator+(Person& p) {
Person temp;
temp.m_A = p.m_A + this->m_A;
temp.m_B = p.m_B + this->m_B;
return temp;
}
private:
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;
}
//3. << 运算符重载:必须是全局函数
ostream& operator<<(ostream & cout,Person &p) {
cout << "m_A = " << p.m_A << ", m_B = " << p.m_B;
return cout;
}
//测试
void test01()
{
Person p1;
Person p2;
Person p3 = p1 + p2;
cout << p1 <<endl; //注意这儿输出使用到了 << 重载
cout << p3 << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
4.5.2 << 运算符重载
#include<iostream>
using namespace std;
class Cat {
friend ostream& operator<<(ostream& cout, Cat& cat);//友元函数
public:
Cat() {
name = "小黑";
color = "White";
age = 3;
}
private:
string name;
string color;
int age;
};
// << 运算符重载,注意返回值,形参列表。<<只能是全局函数重载
ostream& operator<<(ostream& cout, Cat& cat)
{
cout << "name:" << cat.name << " age:" << cat.age << " color:" << cat.color;
return cout;
}
int main()
{
Cat cat;
cout << cat << endl;
system("pause");
return 0;
}
4.5.3 ++ 运算符重载
注意点:
- 1、前置自增运算符重载,返回引用
- 2、后置自增运算符重载,返回的是对象(值)
- 3、后置自增运算符重载:int 占位参数。形参列表使用int与前置区分,而且只能使用int, double float不好使。
- 后置递增重载返回的是值,而不是返回引用。因为temp是局部变量,局部变量结束时该变量就被销毁了
- 4、在后置递增使用<< 链式输出时,对于<<的运算符重载的形参为 ostream& operator<<(ostream& cout, MyInteger p),即第二个参数非引用传递。因为在后置递增操作中,返回的是新生成的一个对象temp, 而不是原来的对象。因此,是对temp做链式操作。
#include<iostream>
using namespace std;
class MyInteger
{
friend ostream& operator<<(ostream& cout, MyInteger p);
public:
MyInteger()
{
m_Num = 0;
}
MyInteger(int num) {
m_Num = num;
}
//前置自增运算符重载,返回引用
MyInteger& operator++() {
m_Num++;
return *this; //返回自身 *this
}
MyInteger operator++(int)
{
//先记录当时的结果
MyInteger temp = *this;
m_Num++;
return temp;
}
private:
int m_Num;
};
//注意:这里<<运算符重载的 MyInteger p 是值传递,不能传递引用,因为在后置递增操作中,返回的是
//temp,而不是原来的对象。因此,是对temp做链式操作。
ostream& operator<<(ostream& cout, MyInteger p)
{
cout << "m_Num:" << p.m_Num;
return cout;
}
void test301()
{
MyInteger p1(0);
//前置递增
cout << ++(++p1) << endl;
cout << p1 << endl;
}
void test302()
{
//后置递增
MyInteger p2(0);
cout << p2++ << endl;
cout << p2 << endl;
}
int main()
{
test302();
system("pause");
return 0;
}
4.5.4 赋值运算符重载
C++编译器至少给一个类添加4个函数
- 1、默认构造函数(无参、函数体为空)
- 2、默认析构函数(无参、函数体为空)
- 3、默认拷贝构造函数,对属性值进行拷贝
- 4、赋值运算符operator=,对属性进行值拷贝
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3ppf1lNu-1617755828320)(C:\Users\李柏松\AppData\Roaming\Typora\typora-user-images\image-20201127081456103.png)]
编译器提供的是浅拷贝,所以赋值操作将使p1和p2指向同一块内存。导致在调用析构函数时,重复释放同一块内存,从而报错。因此,需要对=进行重载,深拷贝解决。
#include<iostream>
using namespace std;
class Person04
{
public:
//构造函数,在堆上开辟空间,需要程序员手动申请内存,释放内存
Person04(int age) {
m_age = new int(age);
}
~Person04() {
//如果不为空,就释放
if (m_age != NULL) {
delete(m_age);
m_age = NULL;
}
}
//赋值运算符重载
Person04& operator=(Person04& p)
{
//编译器提供的是先拷贝
//m_age = p.m_age;
//应该首先判断是否有属性在堆区,如果有先释放干净
if (m_age != NULL) {
delete m_age;
m_age = NULL;
}
//深拷贝,重新申请一个
m_age = new int(*p.m_age);
return *this;
}
int *m_age;
};
void test04_01() {
Person04 p1(10);
Person04 p2(20);
Person04 p3(30);
//因为编译器的=是浅拷贝,下属的赋值操作将使p1和p2指向同一块内存。
//导致在调用析构函数时,重复释放同一块内存,从而报错。因此,需要对=进行重载,深拷贝解决
p3 = p2 = p1;
cout <<"p1的年龄为:"<< *p1.m_age << endl;
cout << "p2的年龄为:" << *p2.m_age << endl;
cout << "p3的年龄为:" << *p3.m_age << endl;
}
4.5.5 关系运算符重载
class Dog
{
public:
Dog(int age) {
m_Age = age;
}
//比较运算符重载
bool operator==(Dog& d) {
if (m_Age == d.m_Age)
return true;
else
return false;
}
int m_Age;
};
4.5.6 函数调用运算符重载
- 函数调用符 () 也可以重载
- 由于重载后使用的方式非常像函数的调用,因此成为仿函数
- 仿函数没有固定写法,非常灵活
#include<iostream>
using namespace std;
class Person05
{
public:
//函数调用符() 重载
void operator()(string str) {
cout << str << endl;
}
};
int main()
{
Person05 p;
p("hello world");
system("pause");
return 0;
}
4.6 继承
4.6.1 语法
class 子类(派生类): 继承方式 父类(基类)
4.6.2 继承方式
- public继承
- protected继承
- private继承
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U7lH5tvq-1617755828322)(C:\Users\李柏松\AppData\Roaming\Typora\typora-user-images\image-20201127100049782.png)]
4.6.3 继承中的对象模型
父类中所有非静态成员属性都会被子类继承
父类中的私有属性的成员,是被编译器隐藏了。
**利用开发人员工具查看对象模型:**
- 在 visual studio 文件夹中打开 Development Command Prompt for VS 2019 工具;
- 切换到该cpp文件所在的文件夹
- 输入 cl /d1 reportSingleClassLayout类名 文件名.cpp
- 查看01-继承中的对象类型.cpp文件中的Son类: cl /d1 reportSingleClassLayoutSon 01-继承中的对象类型.cpp
01-继承中的对象类型.cpp
class Son1 size(16):
+---
0 | +--- (base class Person)
0 | | m_A
4 | | m_B
8 | | m_C
| +---
12 | son_A
+---
#include<iostream>
using namespace std;
class Person {
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son1 :public Person {
public:
int son_A;
};
int main()
{
cout << "sizeof Son = " << sizeof(Son1) << endl; //输出16
system("pause");
return 0;
}
4.6.4 继承中构造和析构的顺序
执行顺序:父类构造—子类构造—子类析构—父类析构
#include<iostream>
using namespace std;
class Base
{
public:
Base() {
cout << "父类构造函数调用~" << endl;
}
~Base() {
cout << "父类析构函数调用~" << endl;
}
};
class Son :public Base
{
public:
Son() {
cout << "子类构造函数调用" << endl;
}
~Son() {
cout << "子类析构函数调用" << endl;
}
};
void test01()
{
//Base b1;
Son son1;
}
int main()
{
test01();
system("pause");
return 0;
}
4.6.5 继承中父类子类成员同名处理
- 子类可以直接访问子类的同名的成员
- 子类和父类同名成员时,访问父类的成员需要加作用域符号 ::
- 如果子类和父类有同名函数时,子类会隐藏父类中所有的同名成员函数(包括重载的成员函数),因此访问父类成员函数需要加作用域 ::
#include<iostream>
using namespace std;
class Father
{
public:
Father() {
m_A = 100;
}
int m_A;
};
class Son2 :public Father
{
public:
Son2() {
m_A = 200;
}
int m_A;
};
void test03_01()
{
Son2 s;
cout << "Son下的m_A = " << s.m_A << endl;
cout << "Son下的m_A = " << s.Father::m_A << endl;
}
int main()
{
test03_01();
system("pause");
return 0;
}
4.6.6 继承同名静态成员的处理方式
静态成员变量特点:
- 1、所有对象都共享同一份数据
- 2、编译阶段就分配内存
- 3、类内声明,类外初始化
静态成员函数特点:
- 1、所有对象共享同一个函数实例
- 2、只能访问静态成员变量,不能访问非静态成员变量
与非静态成员处理方式一样:访问父类加作用域 :: ,访问自己直接访问
#include<iostream>
using namespace std;
class father04
{
public:
static int m_A; //声明静态变量
static void func() {
cout << "父类静态函数调用" << endl;
}
};
//类外初始化静态变量
int father04::m_A = 100;
class son04:public father04
{
public:
//类内声明静态变量
static int m_A;
static void func() {
cout << "子类静态函数调用" << endl;
}
};
//类外初始化静态变量
int son04::m_A = 200;
void test04_01()
{
son04 s;
cout << "son(m_A) = " << s.m_A << endl;
cout << "father(m_A) = " << s.father04::m_A << endl;
cout << "不创建对象,直接通过类名访问" << endl;
cout << "son(m_A) = " << son04::m_A<< endl;
cout << "father(m_A) = " << son04::father04::m_A<< endl;
son04::func(); //访问子类自己的静态成员函数
son04::father04::func();//访问父类的静态成员函数
}
int main()
{
test04_01();
system("pause");
return 0;
}
4.7 多态
4.7.1 多态的基本概念
C++多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定 — 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 — 运行阶段确定函数地址
动态多态条件:
- 1、存在继承关系
- 2、子类要重写父类的虚函数
- 3、父类的指针或引用指向子类型对象(多态使用方法)
#include<iostream>
using namespace std;
class Animal
{
public:
//virtual 虚函数
virtual void speak() {
cout << "动物在说话" << endl;
}
};
class Cat : public Animal
{
public:
void speak()
{
cout << "小猫咪在说话:喵喵喵~" << endl;
}
};
/*
执行说话的函数
地制早绑定,在编译阶段就确定了函数地址。调用此函数会执行动物类的speak方法
如果想执行cat类的speak方法,那么这个函数地址就不能提前绑定,需要在运行阶段绑定。地址晚绑定
*/
void doSpeak(Animal & animal) //等价于:Animal & animal = cat;
{
animal.speak();
}
void test05_01()
{
Cat cat;
doSpeak(cat);
}
int main()
{
test05_01();
system("pause");
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tNEWsbCC-1617755828325)(E:\C++相关\图\多态底层原理.jpg)]
4.7.2 纯虚函数和抽象类
纯虚函数语法:virtual 返回值类型 函数名(参数列表) = 0;
当类中有了纯虚函数,这个类就叫做抽象类
抽象类特点:
- 1、抽象类无法实例化对象
- 2、子类必须重写抽象类中的纯虚函数,否则也是抽象类
#include<iostream>
using namespace std;
class Birds
{
public:
//纯虚函数。有纯虚函数的类成为抽象类,抽象类无法实例化对象
virtual void run() = 0;
};
class check :public Birds{
public:
void run()
{
cout << "小鸡在跑" << endl;
}
};
void test06_01()
{
Birds* bird = new check;
bird->run();
}
int main()
{
test06_01();
system("pause");
return 0;
}
4.7.3 虚析构和纯虚析构函数
在多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方法:将父类中的析构函数改为虚析构函数或纯虚析构函数
虚析构或纯虚析构共性:
- 1、可以解决父类指针释放子类对象
- 2、都需要有具体的函数实现
区别:
- 如果是纯虚析构,则该类属于抽象类,无法实例化
语法:
//1、虚析构函数
virtual ~类名()
{
//函数体
}
//2、纯虚析构函数
virtual ~类名() = 0; //
//在类外对纯虚析构函数进行实现
类名::~类名()
{
}
#include<iostream>
using namespace std;
class Animal07
{
public:
Animal07() {
cout << "Animal的构造函数调用" << endl;
}
//1、虚析构函数
/*virtual ~Animal07()
{
cout << "Animal的虚析构函数调用" << endl;
if (m_name != NULL) {
delete m_name;
m_name = NULL;
}
}*/
//2、纯虚析构函数
virtual ~Animal07() = 0;
virtual void speak() = 0;
string* m_name;
};
//类外实现Animal的纯虚析构函数实现
Animal07::~Animal07() {
cout << "Animal的纯虚析构函数调用" << endl;
if (Animal07::m_name != NULL) {
delete Animal07::m_name;
Animal07::m_name = NULL;
}
}
class Cat07 :public Animal07
{
public:
Cat07(string name) {
m_name = new string(name);
cout << "Cat类的构造函数调用" << endl;
}
~Cat07()
{
cout << "Cat类的析构函数调用" << endl;
if (m_name != NULL) {
delete m_name;
m_name = NULL;
}
}
void speak()
{
cout << *m_name << "在说话" << endl;
}
string* m_name;
};
void test07_01()
{
/*下面这段程序打印:
Animal的构造函数调用
Cat类的构造函数调用
Tom在说话
Animal的析构函数调用
存在的问题:不能释放子类,造成内存泄漏。解决方法:将父类的析构函数改为虚析构函数
*/
Animal07* cat = new Cat07("Tom");
cat->speak();
//释放内存
delete cat;
}
int main()
{
test07_01();
system("pause");
return 0;
}
总结:
- 1、虚析构和纯虚析构函数都是为了解决通过父类指针释放子类对象
- 2、如果子类中没有堆区数据,可以不写虚析构和纯虚析构函数
- 3、拥有纯虚析构函数的类也属于抽象类,不能被实例化
五、文件操作
C++中对文件操作需要包含头文件 <fstream>
文件类型分为两类:
- 1、文本文件:文件以ASCII码形式存储在计算机中
- 2、二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能读懂
操作文件的三大类:
- 1、ofstream:写操作
- 2、ifstream:读操作
- 3、fstream:读写操作
5.1 文本文件
5.1.1 写文件
写文件的步骤:
- 1、包含头文件:#include<fstream>
- 2、创建流对象:ofstream ofs;
- 3、打开文件:ofs.open(“文件路径”,打开方式);
- 4、写数据:ofs<<“写入的数据”;
- 5、关闭文件:ofs.close();
文件打开方式:
打开方式 | 解释 |
---|---|
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在先删除,再创建 |
ios::binary | 二进制方式 |
注意:文件打开方式可以配合使用:利用 “ | ” 操作符
以二进制的方式写文件:ios::binary | ios::out
#include<iostream>
using namespace std;
#include<fstream>
void test01_01() {
//文件操作5步:
//1、包含头文件 #include<fstream>
//2、创建流对象。写的流对象是ofstream
ofstream ofs;
//ifstream ifs; //读文件流
//3、指定打开文件的方式 ofs.open("文件路径",打开方式);
ofs.open("test01.txt",ios::out);
//4、写数据
ofs << "姓名:李柏松" << endl;
ofs << "性别:男" << endl;
ofs << "生日:1995-10-12" << endl;
//5、关闭流
ofs.close();
}
int main()
{
test01_01();
system("pause");
return 0;
}
5.1.2 读文件
读文件步骤如下:
- 1、包含头文件 #include<fstream>
- 2、创建流对象 ifstream ifs;
- 3、打开文件并判断是否打开成功 ifs.open(“文件路径”, 打开方式);
- 4、读数据: 四种方式读取
- 5、关闭文件:ifs.close()
#include<iostream>
using namespace std;
#include<fstream>
#include<string>
void test02_01()
{
//读文件5步
//1、包含头文件
//2、创建读取流对象
ifstream ifs;
//3、打开文件并判断是否成功
ifs.open("test01.txt",ios::in);
if (!ifs.is_open()) { //判断是否打开成功
cout << "文件打开失败" << endl;
return;
}
//4、读数据
//第一种方式
/*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;
}
//5、关闭文件
ifs.close();
}
int main()
{
test02_01();
system("pause");
return 0;
}
5.2 二进制文件
以二进制的方式对文件进行读写操作,打开方式要指定为 ios::binary
5.2.1 写文件
利用二进制方式写文件要利用流对象进行 write 操作
函数原型:ofs.write((const char*)&buf, int len);
#include<iostream>
using namespace std;
#include<fstream>
//二进制方式可以操作自定义数据类型
class Person
{
public:
char name[64];
int age;
};
void test03_01()
{
//1、包含头文件
//2、创建流对象
ofstream ofs;
//3、打开文件
ofs.open("person.txt", ios::out | ios::binary);
//4、写数据
Person p = { "张三", 19 };
ofs.write((const char*)&p, sizeof(Person));
//5、关闭文件
ofs.close();
}
5.2.2 读文件]
利用流对象read函数
函数原型:ifs.read( char* buffer, int len);
buffer: 指向内存中的一段存储空间,len是读写的字符数
//3、打开文件
ifs.open("person.txt", ios::in | ios::binary);
//判断文件是否打开成功
if (!ifs.is_open()) {
cout << "文件打开失败" << endl;
return;
}
Person p; //创建接受文件数据的对象
ifs.read((char *)&p, sizeof(Person)); //读取文件
cout << "姓名:" << p.name << " 年龄:" << p.age << endl;
#include<iostream>
using namespace std;
#include<fstream>
class Person
{
public:
char name[64];
int age;
};
void test04_01()
{
//2、创建流对象
ifstream ifs;
//3、打开文件
ifs.open("person.txt", ios::in | ios::binary);
//判断文件是否打开成功
if (!ifs.is_open()) {
cout << "文件打开失败" << endl;
return;
}
Person p; //创建接受文件数据的对象
ifs.read((char *)&p, sizeof(Person)); //读取文件
cout << "姓名:" << p.name << " 年龄:" << p.age << endl;
//关闭文件
ifs.close();
}
int main()
{
test04_01();
system("pause");
return 0;
}
是否打开成功
cout << “文件打开失败” << endl;
return;
}
//4、读数据
//第一种方式
/*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;
}
//5、关闭文件
ifs.close();
}
int main()
{
test02_01();
system("pause");
return 0;
}
### 5.2 二进制文件
以二进制的方式对文件进行读写操作,打开方式要指定为 ios::binary
#### 5.2.1 写文件
利用二进制方式写文件要利用流对象进行 write 操作
==函数原型:ofs.write((const char*)&buf, int len);==
```c++
#include<iostream>
using namespace std;
#include<fstream>
//二进制方式可以操作自定义数据类型
class Person
{
public:
char name[64];
int age;
};
void test03_01()
{
//1、包含头文件
//2、创建流对象
ofstream ofs;
//3、打开文件
ofs.open("person.txt", ios::out | ios::binary);
//4、写数据
Person p = { "张三", 19 };
ofs.write((const char*)&p, sizeof(Person));
//5、关闭文件
ofs.close();
}
5.2.2 读文件]
利用流对象read函数
函数原型:ifs.read( char* buffer, int len);
buffer: 指向内存中的一段存储空间,len是读写的字符数
//3、打开文件
ifs.open("person.txt", ios::in | ios::binary);
//判断文件是否打开成功
if (!ifs.is_open()) {
cout << "文件打开失败" << endl;
return;
}
Person p; //创建接受文件数据的对象
ifs.read((char *)&p, sizeof(Person)); //读取文件
cout << "姓名:" << p.name << " 年龄:" << p.age << endl;
#include<iostream>
using namespace std;
#include<fstream>
class Person
{
public:
char name[64];
int age;
};
void test04_01()
{
//2、创建流对象
ifstream ifs;
//3、打开文件
ifs.open("person.txt", ios::in | ios::binary);
//判断文件是否打开成功
if (!ifs.is_open()) {
cout << "文件打开失败" << endl;
return;
}
Person p; //创建接受文件数据的对象
ifs.read((char *)&p, sizeof(Person)); //读取文件
cout << "姓名:" << p.name << " 年龄:" << p.age << endl;
//关闭文件
ifs.close();
}
int main()
{
test04_01();
system("pause");
return 0;
}