文章目录
- C++基础
- 常量
- 数据类型
- 冒泡排序
- 指针
- 结构体
- c++核心
- 内存分区模型
- 程序运行前
- 程序运行后
- 引用
- 指针和引用的区别
- 函数的默认参数
- 函数重载
- 重载和重写的区别
- 类和对象
- 封装
- struct和class的区别
- 构造函数和析构函数
- 拷贝构造
- 深拷贝和浅拷贝
- 静态成员
- C++对象模型和this指针
- 成员变量和成员函数分开存储
- this指针
- const修饰成员函数
- 友元
- 继承
- 继承中构造和析构顺序
- 继续中同名函数处理
- 多继承
- 多态
- 多态的基本概念
- 纯虚函数和抽象类
- 虚析构和纯虚函数析构
- 文件操作
- 写文件
- 读文件
C++基础
常量
C++定义常量两种方式
- #define宏定义:
define 常量名 常量值
- const修饰的变量
const 数据类型 常量名=常量值
数据类型
-
字符串型
c风格字符型
: char 变量名[]=“字符串值”C++ 风格字符串
: string 变量名=“字符串值”
冒泡排序
最常用的排序算法,对数组内元素进行排序。
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,执行完毕后,找到第一个最大值。
- 重复以上的步骤,每次比较次数-1,直到不需要比较
示例: 将数组 { 4,2,8,0,5,7,1,3,9 } 进行升序排序
//冒泡排序函数
void bubbleSort(int * arr, int len) //int * arr 也可以写为int arr[]
{for (int i = 0; i < len - 1; i++){for (int j = 0; j < len - 1 - i; j++){if (arr[j] > arr[j + 1]){int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}
}//打印数组函数
void printArray(int arr[], int len)
{for (int i = 0; i < len; i++){cout << arr[i] << endl;}
}int main() {int arr[10] = { 4,3,6,9,1,2,10,8,7,5 };int len = sizeof(arr) / sizeof(int);bubbleSort(arr, len);printArray(arr, len);system("pause");return 0;
}
指针
1.指针变量和普通变量的区别
- 普通变量存放的是数据,指针变量存放的是地址
- 指针变量可以通过" * "操作符,操作指针变量指向的内存空间,这个过程称为解引用
2.空指针和野指针
-
空指针:指针变量指向内存中编号为0的空间
-
**用途:**初始化指针变量
-
**注意:**空指针指向的内存是不可以访问的
-
**示例:**空指针
int main() {//指针变量p指向内存地址编号为0的空间int * p = NULL;//访问空指针报错 //内存编号0 ~255为系统占用内存,不允许用户访问cout << *p << endl;system("pause");return 0;
}
-
野指针:指针变量指向非法的内存空间
-
示例2:野指针
int main() {//指针变量p指向内存地址编号为0x1100的空间int * p = (int *)0x1100;//访问野指针报错 cout << *p << endl;system("pause");return 0;
}
- 总结:空指针和野指针都不是我们申请的空间,因此不要访问。
3.智能指针
智能指针就是一个类,当超出类的作用域时,类会自动调用析构函数,自动释放资源。
智能指针主要管理在一个指针,因为申请的空间在函数结束时忘记释放了造成内存泄露。使用智能指针可以避免这个问题。
4.const修饰指针
const修饰指针有三种情况
- const修饰指针 — 常量指针
- const修饰常量 — 指针常量
- const即修饰指针,又修饰常量
int main() {int a = 10;int b = 10;//const修饰的是指针,指针指向可以改,指针指向的值不可以更改const int * p1 = &a; p1 = &b; //正确//*p1 = 100; 报错//const修饰的是常量,指针指向不可以改,指针指向的值可以更改int * const p2 = &a;//p2 = &b; //错误*p2 = 100; //正确//const既修饰指针又修饰常量const int * const p3 = &a;//p3 = &b; //错误//*p3 = 100; //错误system("pause");return 0;
}
- 技巧:看const右侧紧跟着的是指针还是常量, 是指针就是常量指针,是常量就是指针常量
结构体
结构体属于用户自定义的数据类型,允许用户存储不同的数据类型
c++核心
内存分区模型
1.C++程序在执行时,将内存大方向划分为4个区域
- 代码区:存放函数体的二进制代码,由操作系统进行管理的
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
2.内存分区的意义
不同区域存放的数据,赋予不同的生命周期, 给我们更大的灵活编程
程序运行前
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域
? 代码区:
? 存放 CPU 执行的机器指令
? 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
? 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
? 全局区:
? 全局变量和静态变量存放在此.
? 全局区还包含了常量区, 字符串常量和其他常量也存放在此.
? 该区域的数据在程序结束后由操作系统释放.
程序运行后
? 栈区:
? 由编译器自动分配释放, 存放函数的参数值,局部变量等
? 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
? 堆区:
? 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
? 在C++中主要利用new在堆区开辟内存
引用
引用的本质:给数据类型起别名
数据类型 &别名=原名
- 引用必须初始化
- 引用在初始化后,不可以改变
本质:引用的本质在c++内部实现是一个指针常量.
//发现是引用,转换为 int* const ref = &a;
void func(int& ref){ref = 100; // ref是引用,转换为*ref = 100
}
int main(){int a = 10;//自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改int& ref = a; ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;cout << "a:" << a << endl;cout << "ref:" << ref << endl;func(a);return 0;
}
指针和引用的区别
**指针:**是一个变量类型;指针可以不进行初始化;指针初始化后可以改变,在写代码时需要大量的检测
**引用:**是一个别名;引用必须要初始化;引用初始化后不可改变,无需检测
函数的默认参数
在C++中,函数的形参列表中的形参是可以有默认值的。
语法:返回值类型 函数名 (参数= 默认值){}
int func(int a, int b = 10, int c = 10) {return a + b + c;
}
//1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
//2. 如果函数声明有默认值,函数实现的时候就不能有默认参数
函数重载
**作用:**函数名可以相同,提高复用性
函数重载满足条件:
- 同一个作用域下
- 函数名称相同
- 函数参数类型不同 或者 个数不同 或者 顺序不同
注意: 函数的返回值不可以作为函数重载的条件
重载和重写的区别
**重载:**是指函数名相同,而这些函数的参数表不同。
**重写:**是指子类重新定义父类虚函数的方法。和多态相关。
类和对象
C++面向对象的三大特性为:封装、继承、多态
C++认为==万事万物都皆为对象,对象上有其属性和行为
封装
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
封装意义一:
? 在设计类的时候,属性和行为写在一起,表现事物
封装意义二:
类在设计时,可以把属性和行为放在不同的权限下,加以控制
访问权限有三种:
- public 公共权限
- protected 保护权限
- private 私有权限
struct和class的区别
在C++中 struct和class唯一的区别就在于 默认的访问权限不同
区别:
- struct 默认权限为公共
- class 默认权限为私有
构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题
? 一个对象或者变量没有初始状态,对其使用后果是未知
? 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
c++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供
编译器提供的构造函数和析构函数是空实现。
-
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
-
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
拷贝构造
C++中拷贝构造函数调用时机通常有三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
深拷贝和浅拷贝
**浅拷贝:***简单的赋值拷贝操作
没有给新的指针申请内存来存内容,新的对象里面的指针和传进的指针都是指向同一块内存
**深拷贝:**在堆区重新申请空间,进行拷贝操作
给新的指针申请内存来存内存,这样就不会出现浅拷贝所出现的问题了
静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
C++对象模型和this指针
成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
**《非静态成员变量占对象空间
int mA;
静态成员变量不占对象空间
static int mB;》 **
class Person {
public:Person() {mA = 0;}//非静态成员变量占对象空间int mA;//静态成员变量不占对象空间static int mB; //函数也不占对象空间,所有函数共享一个函数实例void func() {cout << "mA:" << this->mA << endl;}//静态成员函数也不占对象空间static void sfunc() {}
};int main() {cout << sizeof(Person) << endl;system("pause");return 0;
}
this指针
this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
const修饰成员函数
常函数:
- 成员函数后加const后我们称为这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
class Person {
public:Person() {m_A = 0;m_B = 0;}//this指针的本质是一个指针常量,指针的指向不可修改//如果想让指针指向的值也不可以修改,需要声明常函数void ShowPerson() const {//const Type* const pointer;//this = NULL; //不能修改指针的指向 Person* const this;//this->mA = 100; //但是this指针指向的对象的数据是可以修改的//const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量this->m_B = 100;}void MyFunc() const {//mA = 10000;}public:int m_A;mutable int m_B; //可修改 可变的
};//const修饰对象 常对象
void test01() {const Person person; //常量对象 cout << person.m_A << endl;//person.mA = 100; //常对象不能修改成员变量的值,但是可以访问person.m_B = 100; //但是常对象可以修改mutable修饰成员变量//常对象访问成员函数person.MyFunc(); //常对象不能调用const的函数}int main() {test01();system("pause");return 0;
}
友元
友元的目的就是让一个函数或者类 访问另一个类中私有成员
友元的关键字为 friend
函数调用运算符重载
- 函数调用运算符 () 也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
class MyPrint
{
public:void operator()(string text){cout << text << endl;}};
void test01()
{//重载的()操作符 也称为仿函数MyPrint myFunc;myFunc("hello world");
}class MyAdd
{
public:int operator()(int v1, int v2){return v1 + v2;}
};void test02()
{MyAdd add;int ret = add(10, 10);cout << "ret = " << ret << endl;//匿名对象调用 cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}int main() {test01();test02();system("pause");return 0;
}
继承
**继承的好处:**可以减少重复的代码
class A : public B;
A 类称为子类 或 派生类
B 类称为父类 或 基类
派生类中的成员,包含两大部分:
一类是从基类继承过来的,一类是自己增加的成员。
从基类继承过过来的表现其共性,而新增的成员体现了其个性。
继承中构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数
**总结:**继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
继续中同名函数处理
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
多继承
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
- 利用虚继承可以解决菱形继承问题
多态
多态的基本概念
多态分为两类
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
多态满足条件
- 有继承关系
- 子类重写父类中的虚函数
多态使用条件
- 父类指针或引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
class Animal
{
public://Speak函数就是虚函数//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。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 & animal)
{animal.speak();
}
//
//多态满足条件:
//1、有继承关系
//2、子类重写父类中的虚函数
//多态使用:
//父类指针或引用指向子类对象void test01()
{Cat cat;DoSpeak(cat);Dog dog;DoSpeak(dog);
}int main() {test01();system("pause");return 0;
}
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public://纯虚函数//类中只要有一个纯虚函数就称为抽象类//抽象类无法实例化对象//子类必须重写父类中的纯虚函数,否则也属于抽象类virtual void func() = 0;
};class Son :public Base
{
public:virtual void func() {cout << "func调用" << endl;};
};void test01()
{Base * base = NULL;//base = new Base; // 错误,抽象类无法实例化对象base = new Son;base->func();delete base;//记得销毁
}int main() {test01();system("pause");return 0;
}
虚析构和纯虚函数析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
class Animal {
public:Animal(){cout << "Animal 构造函数调用!" << endl;}virtual void Speak() = 0;//析构函数加上virtual关键字,变成虚析构函数//virtual ~Animal()//{// cout << "Animal虚析构函数调用!" << endl;//}virtual ~Animal() = 0;
};Animal::~Animal()
{cout << "Animal 纯虚析构函数调用!" << endl;
}//和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。class Cat : public Animal {
public:Cat(string name){cout << "Cat构造函数调用!" << endl;m_Name = new string(name);}virtual void Speak(){cout << *m_Name << "小猫在说话!" << endl;}~Cat(){cout << "Cat析构函数调用!" << endl;if (this->m_Name != NULL) {delete m_Name;m_Name = NULL;}}public:string *m_Name;
};void test01()
{Animal *animal = new Cat("Tom");animal->Speak();//通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏//怎么解决?给基类增加一个虚析构函数//虚析构函数就是用来解决通过父类指针释放子类对象delete animal;
}int main() {test01();system("pause");return 0;
}
总结:
? 1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
? 2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
? 3. 拥有纯虚析构函数的类也属于抽象类
文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放
通过文件可以将数据持久化
C++中对文件操作需要包含头文件 < fstream >
文件类型分为两种:
- 文本文件 - 文件以文本的ASCII码形式存储在计算机中
- 二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
- ofstream:写操作
- ifstream: 读操作
- fstream : 读写操作
写文件
写文件步骤如下:
-
包含头文件
#include <fstream>
-
创建流对象
ofstream ofs;
-
打开文件
ofs.open(“文件路径”,打开方式);
-
写数据
ofs << “写入的数据”;
-
关闭文件
ofs.close();
#include <fstream>void test01()
{ofstream ofs;ofs.open("test.txt", ios::out);ofs << "姓名:张三" << endl;ofs << "性别:男" << endl;ofs << "年龄:18" << endl;ofs.close();
}int main() {test01();system("pause");return 0;
}
总结:
- 文件操作必须包含头文件 fstream
- 读文件可以利用 ofstream ,或者fstream类
- 打开文件时候需要指定操作文件的路径,以及打开方式
- 利用<<可以向文件中写数据
- 操作完毕,要关闭文件
读文件
读文件步骤如下:
-
包含头文件
#include <fstream>
-
创建流对象
ifstream ifs;
-
打开文件并判断文件是否打开成功
ifs.open(“文件路径”,打开方式);
-
读数据
四种方式读取
-
关闭文件
ifs.close();
#include <fstream>
#include <string>
void test01()
{ifstream ifs;ifs.open("test.txt", ios::in);if (!ifs.is_open()){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() {test01();system("pause");return 0;
}
总结:
- 读文件可以利用 ifstream ,或者fstream类
- 利用is_open函数可以判断文件是否打开成功
- close 关闭文件