这里写目录标题
前言
C++学习笔记
一、内存分区模型
C++程序在执行时,将内存大方向划分为4个区域
- 代码区:存放函数体的二进制代码,由操作系统进行管理的
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值、局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
内存四区的意义:
不同区域存放的数据,赋予不同的生命周期,给予更大的灵活性。
C++中在程序运行前分为全局区和代码区。
常量分为:字符串常量和const修饰的变量
栈区
由编译器自动分配释放,存放函数的参数值,局部变量等
注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
栈区的数据在函数执行完后自动释放
// 指针本质也是局部变量,放在栈上,指针保存的数据是放在堆区
int *p = new int(10);
堆区
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收。在C++中主要利用new在堆区开辟内存,想要释放该内存空间使用delete关键字
delete p;
new的基本语法
int * func(){
// new 返回是 该数据类型的指针
int *p = new int(10);
return p;
}
// new 关键字开辟数组
int * arr = new int[10];
// 释放数组
delete [] arr;
二、引用
引用的本质就是给变量取别名
数据类型 &别名 = 变量名;
注意:
-
引用必须要初始化
int &a; //错误
2.引用一旦初始化就不能更改
int a = 10;
int &b = a;
int c = 20;
b = c; // 这是赋值操作,不是更改引用
引用做函数参数
作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参
// 值传递 形参不会修饰实参
void swap01(int a, int b){
int temp = a;
a = b;
b = temp;
}
// 地址传递 形参会修饰实参
void swap02(int * a, int * b){
int temp = *a;
*a = *b;
*b = temp;
}
// 引用传递 形参会修饰实参
void swap03(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
int main(){
int a = 10;
int b = 20;
swap01(a, b);
cout << a << endl; // 10
cout << b << endl; // 20
swap02(&a, &b);
cout << a << endl; // 20
cout << b << endl; // 10
swap03(a, b);
cout << a << endl; // 10
cout << b << endl; // 20
}
引用传递能修改实参的原因是因为引用的本质实参的别名,即还是将实参传递到交换函数里面了,而值传递则是重新复制了一份实参。
引用做函数返回值
作用:引用是可以作为函数的返回值存在的
注意:不要返回局部变量引用
用法:函数作为左值
int& test(){
static int a = 10; // 静态变量 存放在全局区
return a;
}
int main(){
int& b = test();
cout << b << endl; // 10
cout << b << endl; // 10
test() = 100;
cout << b << endl; // 100
cout << b << endl; // 100
}
test()返回的是a 变量, 即a=100; 而b是a的别名。
引用的本质
引用的本质在C++内部实现就是一个指针常量!
int a=10;
int &ref = a;
ref = 20; // 内部自动帮我们转换:*ref=20;
由于引用是指针常量,故一旦初始化就不能修改!
常量引用
作用:常量引用主要用来修饰形参,防止误操作。
在函数形参列表中,可以加const修饰形参,防止形参改变实参。
int a=10;
// int &ref = 10; // 报错,引用必须引一块合法的内存空间
const int &ref = 20; // 可以,编译器将代码进行了修改 int temp =20; const int &ref = temp;
void show(const int & val){
}
三、函数重载
作用:函数名可以相同,提高复用性
函数重载满足条件:
- 同一个作用域下
- 函数名称相同
- 函数参数类型不同 或 个数不同 或 顺序不同
注意:函数的返回值不作为重载的条件
四、类和对象
访问权限:public, protected, private
- public: 成员类内可以访问,类外不可以访问
- protected:类内可以访问,类外不可以访问,子类可以访问父类中的protected属性
- private:类内可以访问,类外不可以访问
struct和class的区别:默认的访问权限不同
- struct默认权限为公共
- class默认权限为私有
对象的初始化和清理
构造函数和析构函数
- 构造函数:在创建对象时为对象的成员属性赋值,构造函数由编译器自动调用
- 析构函数:在对象销毁前系统自动调用,执行一些清理工作。
编译器提供的构造函数和析构函数是空实现。
/*
构造函数:
类名(){} // 可以有参数,即可以重载
析构函数:
~类名(){} // 不可以有参数
*/
构造函数分类及调用
分类方式:
- 按参数分 :有参构造和无参构造
- 按类型分:普通构造和拷贝构造
调用方式:
- 括号法
- 显示法
- 隐式转换法
class Person{
public:
Person(){
cout << "无参构造" << endl;
}
Person(int a){
cout << "有参构造" << endl;
}
Person(const Person &p){ // 拷贝构造函数
cout << "拷贝构造函数" << endl;
age = p.age;
}
~Person(){
cout << "析构函数" << endl;
}
private:
int age;
};
int main(){
// 调用
// 括号法
Person p; // 默认构造
// Person p(); 编译器会认为是函数的声明 void test();
Person p1(10); // 有参构造
Person p2(p); // 拷贝构造函数
// 显示法
Person p3 = Person(10); // 有参构造
Person p4 = Person(p3); // 拷贝构造
Person(10); // 匿名对象 特点:当前行执行结束后,系统会立即回收掉匿名对象
/*
注意:不要利用拷贝函数 初始化匿名对象 编译器会认为 Person(p3) === Person p3;
*/
// 隐式转换法
Person p5 = 10; // 等价于 Person p5 = Person(10);
Person p6 =p5 ;// 调用拷贝构造
}
拷贝构造函数的调用时机
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数传值
- 以值方式返回局部对象
class Person{
public:
Person(){
cout << "无参构造" << endl;
}
Person(int a){
age = a;
cout << "有参构造" << endl;
}
Person(const Person &p){ // 拷贝构造函数
cout << "拷贝构造函数" << endl;
age = p.age;
}
~Person(){
cout << "析构函数" << endl;
}
private:
int age;
};
// 值传递的方式给函数传值
void doWork(Person p){
}
void test(){
Person p;
doWork(p);
}
// 值方式返回局部对象
Person doWork02(){
Person p; // 值方式返回会拷贝一份p的副本返回
cout << (long)&p << endl; // 6160232364
return p;
}
void test02(){
Person p = doWork02();
cout << (long)&p << endl; //6160232364
}
int main(){
//test();
test02();
}
按老师的讲解应该会调用拷贝函数,但是我的没有调用,两份地址也一样!
构造函数的调用规则
默认情况下,C++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,C++不在提供默认构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
注意:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。
初始化列表
作用:初始化属性
语法:构造函数():属性1(值1),属性2(值2)…{}
class Person{
public:
Person(int age):age(age){
}
private:
int age;
};
对象成员属性
当其他类对象作为本类成员,构造时先构造类对象,再构造自身,析构的顺序与构造相反
静态成员属性
静态成员就是在成员变量和成员函数前加上static,称为静态成员。
静态成员分为:
- 静态成员变量:
1、所有对象共享同一份数据
2、在编译阶段分配内存 // 全局区
3、类内声明,类外初始化 - 静态成员函数
1、所有对象共享一个函数
2、静态成员函数只能访问静态成员变量
//静态成员变量
class Person{
public:
/*
1、所有对象都共享同一份数据
2、编译阶段就分配内存
3、类内声明,类外初始化操作
*/
static int m_A;
};
int Person::m_A = 100; // 通过类名或对象访问
C++对象模型和this指针
成员变量和成员函数分开存储
在C++中,类中的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上,可以通过sizeof测试!
空对象占1个字节大小
class Person{}
this指针
this指针是一个指针常量
this指针指向被调用的成员函数所属的对象
this指针式隐含每一个非静态成员函数内的一种指针,不需要定义
this指针用途:
- 当行参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可用return *this
class Person{
public:
int age;
Person(int age){
this->age = age;
}
Person addAge(int age){ // 返回一个新的对象
this->age += age;
return *this;
}
Person& addAgeA(int age){ // 返回同一个对象
this->age += age;
return *this;
}
};
int main(){
int age = 10;
Person p(10);
// p.addAge(age).addAge(age);
// cout << p.age << endl; // 20
p.addAgeA(age).addAgeA(age);
cout << p.age << endl; // 30
}
空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针,如果用到this指针,需要加以判断保证代码的健壮性。
if(this == NULL){
return;
}
const修饰成员函数
常函数:
- 成员函数后加const后我们称这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
class Pserson{
public:
int age;
mutable int a;
// 在成员函数后面加const,修饰的是this指向,让指针指向的值也不可以修改
void show() const{
// this->age = 10; 报错
this->a = 10;
}
};
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
const Person p; // 常对象
p.a = 100; // 可以修改
友元
友元的关键为firend,目的是让一个函数或者类访问另一个类中的私有成员
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
class Buliding{
friend void test(Building *building);
private:
int age;
};
void test(Building *building){
cout << building->age << endl;
}
运算符重载
定义:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
加号运算符重载
class Person{
public:
int a;
int b;
// 成员函数重载,也可以使用全局函数重载
Person operator+ (Person &p){
Person temp;
temp.a = this->a + p.a;
temp.b = this->b + p.b;
return temp;
}
};
int main(){
Person p1;
p1.a = 10;
p1.b = 20;
Person p2;
p2.a = 10;
p2.b = 20;
Person p3 = p1 + p2;
cout<< p3.a << p3.b<< endl;
}
注意:对于内置的数据类型的表达式的运算符是不能改变的
<<运算符重载
class Person{
public:
int a;
int b;
Person operator+ (Person &p){
Person temp;
temp.a = this->a + p.a;
temp.b = this->b + p.b;
return temp;
}
};
ostream& operator<<(ostream &cout, Person &a){
cout << "a=" << a.a << "b=" << a.b;
return cout;
}
int main(){
Person p1;
p1.a = 10;
p1.b = 20;
Person p2;
p2.a = 10;
p2.b = 20;
Person p3 = p1 + p2;
cout<< p3.a << p3.b<< endl;
cout << p3 << endl;
}
++运算符重载
class MyInteger{
friend void test();
private:
int num;
public:
MyInteger& operator++(){
num++;
return *this; // 返回引用
}
MyInteger operator++(int ){ // int代表占位参数,用于区分前置和后置递增
MyInteger temp = *this;
num++;
return temp; // 返回值 局部变量
}
};
void test(){
MyInteger number;
number.num = 1;
cout << ++number.num << endl;
cout << number.num++ << endl;
}
int main(){
test();
}
函数调用运算符重载
- 函数调用运算符()也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为防函数
- 防函数没有固定写法,非常灵活
继承
语法:class 子类:继承方式 父类
class Father{};
class Son: public Father{};
继承方式
- 公共继承
- 保护继承
- 私有继承
注意:父类中所有非静态成员属性都会被子类继承,父类中私有成员属性是被编译器隐藏了,因此是访问不到,但是确实被继承了!
class Base{};
class Son: public Base{};
继承同名成员处理方式
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
- 如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名的成员函数,需要加作用域访问
class Base{
public:
Base(){
m_A = 100;
}
int m_A;
void show(){
cout << "base show()" << endl;
}
};
/*
如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名的成员函数
*/
class Son: public Base{
public:
Son(){
m_A = 200;
}
void show(){
cout << "son show()" << endl;
}
int m_A;
};
void test(){
Son s;
cout << s.m_A << endl; // 200
cout << s.Base::m_A << endl; // 100
s.show();
s.Base::show();
}
int main(){
test();
}
多继承语法
class 子类:继承方式 父类1, 继承方式 父类2…
多态
多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态区别: - 静态多态的函数地址早绑定-编译阶段确定函数地址
- 动态多态的函数地址晚绑定-运行阶段确定函数地址
动态多态满足条件:
- 有继承关系
- 子类重写父类的虚函数
动态多态的使用:
父类的指针或引用指向子类对象
由于类中成员变量和成员函数分开存储,虚函数存储在类内部,存在一个vfptr(虚函数指针)指向虚函数表,虚函数表中存放类中的虚函数的地址。当
Animal & animal = cat;
//等价于
cat.speak();
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
纯虚函数语法:
virtual 返回值类型 函数名(参数列表)= 0;
当类中有了纯虚函数,这个类也称为抽象类
抽象类的特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构的区别:
- 如果时纯虚析构,该类属于抽象类,无法实例化
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
虚析构实现:
class Animal{
public:
virtual void speak() = 0;
Animal(){
cout << "animal 的构造函数" << endl;
}
virtual ~Animal(){
cout << "animal 的析构函数" << endl;
}
};
class Cat: public Animal{
public:
Cat(string name){
cout << "cat 的构造函数" << endl;
this->name = new string(name);
}
virtual void speak(){
cout << *name << "hello" << endl;
}
~Cat(){
if(name != NULL){
cout << "cat的析构函数" << endl;
delete name;
name = NULL;
}
}
string * name;
};
void test(){
Animal * animal = new Cat("Tom");
animal->speak();
delete animal;
}
int main(){
test();
}
纯虚析构实现:
class Animal{
public:
virtual void speak() = 0;
Animal(){
cout << "animal 的构造函数" << endl;
}
// virtual ~Animal(){
// cout << "animal 的析构函数" << endl;
// }
/*
纯虚析构 需要申明也需要实现;
有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
*/
virtual ~Animal() = 0;
};
Animal::~Animal(){
cout << "animal 的纯虚析构函数" << endl;
}
class Cat: public Animal{
public:
Cat(string name){
cout << "cat 的构造函数" << endl;
this->name = new string(name);
}
virtual void speak(){
cout << *name << "hello" << endl;
}
~Cat(){
if(name != NULL){
cout << "cat的析构函数" << endl;
delete name;
name = NULL;
}
}
string * name;
};
void test(){
Animal * animal = new Cat("Tom");
animal->speak();
delete animal;
}
int main(){
test();
}
文件操作
C++中对文件操作需要包含头文件 < fstream>
文件类型分为两种:
- 文本文件:文件以文本的ASCII码形式存储在计算机中
- 二进制文件:文件以文本的二进制形式存储在计算机中
操作文件的三大类:
- ofstream: 写操作
- ifstream: 读操作
- fstream: 读写操作
读文件
#include <iostream>
#include <fstream>
using namespace std;
void test(){
ofstream ofs;
ofs.open("test.txt", ios::out);
ofs << "hello world"<<endl;
ofs << "hello python"<<endl;
ofs.close();
}
void read(){
ifstream ifs;
ifs.open("test.txt", ios::in);
if(!ifs.is_open()){
cout << "文件打开失败" << endl;
return;
}
// 读数据 4种方式
// 1
// char buf[1024] = {0};
// while(ifs >> buf){
// cout << buf << endl;
// }
// 2
// char buf[1024];
// 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;
}
ifs.close();
}
int main(){
read();
}
#include <iostream>
using namespace std;
#include <fstream>
class Person{
public:
int age;
string name;
Person(int age, string name){
this->age = age;
this->name = name;
}
};
void test(){
ofstream ofs("person.txt", ios::out | ios::binary);
Person p(19, "zhangsan");
ofs.write((const char * )&p, sizeof(p));
ofs.close();
}
int main(){
test();
}