C++黑马教程阶段2 学习笔记
该文章为 学习C++黑马教程阶段2后的 原创笔记
教程跳转
1. 内存分区
- 主要就是明白内存的一个大致分布 先了解用户区的一些内存分区: 代码区,全局区,栈区,堆区
- 然后每个区最基本的认识:
- 代码区:存储二进制代码
- 全局区:存储全局变量,静态变量,常量
- 栈区:函数是存储在栈区,函数执行完后,函数内部的数据由编译器自动释放
- 堆区:用户可以手动分配资源在堆区,所以也要用户手动释放
2. 引用 (C++特性,C不支持)
首先明白引用 其实就是 变量(一块内存)的别名,所以引用必须初始化,绑定已有对象(如果没有已有对象,又哪来的别名呢)
引用绑定后就不允许更改了
函数传参的时候,之前为了让实参改变,传入的是对象地址,然后函数参数里 用指针接收,函数内部还需要解引用操作,获取对象的值;
C++ 使用引用封装了这个指针传递的操作;可以直接达到影响实参的效果;传参是直接传入对象本身,然后函数参数 用引用接收,写法更加方便;引用的本质 就是 指针常量; 出现引用后编译器自动转化 int &a => int * const a a => *a ; 指针常量指向不可改变,所以这也是为什么引用初始化后不可更改的原因;
const引用的必要性,有的时候函数参数是引用,但是我们不想改变实参,可以用const修饰引用,不影响实参;
3. C C++ 函数部分区别
C++函数还支持 默认参数,占位参数
C/C++ 都支持函数重载;
4. 类和对象
C++里引入了类,虽然C里没有类,但是C里其实也有类的理念,比如int类变量,double类变量;
类其实就是为对象服务的,比如说我们想要一个a类对象,那我们就设计一个a类,然后用a类实例出一个a类对象;
在之前的通讯录项目,也是这么做的;我们需要通讯录对象,联系人对象;这些对象包含很多信息,封装成一个结构体;C++里我们直接封装成类就行了;类里面的东西都是成员 所以有成员变量(属性),成员函数(行为);
类有三种访问权限,分别是public,private,protected;一般来说我们会将成员变量 设为 private权限,防止外界读写,同时方便自己写的时候设置写入检测;
struct 默认权限为公共,class 默认权限为私有 了解即可
类中会自动创建构造函数和析构函数,这些都封装在类里,方便初始化和销毁;
构造函数,主要关注拷贝构造函数 以及 常用的调用方式,括号法和显示法
拷贝构造函数:我们需要知道什么时候调用了拷贝构造函数,函数直接返回对象,传入对象,包括用对象初始化对象的时候,就调用了拷贝构造函数;
拷贝构造函数 相当于 创建了原对象的副本(赋值拷贝) 相当于新建了一块内存 (存储了和原对象相同的内容);构造函数 分为 无参,有参,拷贝;编译器至少需要无参构造和拷贝构造两个构造函数,所以会 自动提供 默认无参构造函数(空实现)以及 默认拷贝构造函数;
然后如果定义有参,编译器就只自动提供拷贝构造函数,如果定义拷贝构造函数,编译器就不提供其他的构造函数了;
无参构造 有参构造 拷贝构造, 如果用户定义某种构造函数,编译器就只会提供它之后的构造函数,前面的构造函数不再提供;而当类内部没有无参构造,和拷贝构造的时候,就会出错!深拷贝,浅拷贝问题 我们知道浅拷贝是简单的赋值拷贝,所以对于地址,它也是直接赋值拷贝,那么如果类中有成员变量是指针的话(存储地址),经过拷贝后,就会导致多个对象里的指针指向同一块内存;
然后之后析构的时候,就会导致堆区的内存重复释放,报错;解决办法:深拷贝,也就是新对象在拷贝原对象的指针变量的时候,重新在堆开辟一块内存,然后让新对象的指针指向新开辟的内存,这样销毁变量的时候,就不会导致重复销毁的问题了;
ps:我们在设计类的时候,需要考虑内存分区;例如我们想要一个身高对象,这是一个int类对象,如果存储在栈区,那我们直接定义 int类对象就行,但如果存储在堆区,我们只能拿到int类对象的地址,所以定义一个int类指针;C++中 万物即对象 所以可以 变量 对象 两词可以互换静态成员
- 静态成员变量 就是 所有变量共享同一个副本,然后它是存储在全局区,它在类内部仅仅是声明,而声明是不分配内存的,所以我们需要在类外部定义它,以来分配静态成员变量的内存;
- 静态成员函数,只需要了解一点,那就是静态成员函数没有this指针,所以静态成员函数里无法访问普通成员,只能访问静态成员;
- 调用方面 静态成员变量和函数 都可以 通过对象和类名::访问;
对象模型,this指针
只有非静态成员变量占对象空间(属于类的对象上);其余 成员函数(非静态,静态)和静态成员变量都不占对象空间;因为静态成员分配在全局区;
空对象比较特殊,它占一个字节 原因:编译器会给每个空对象分配一个字节,区分空对象占内存的位置
首先我们要明确为什么要有this指针
成员函数只会有一份实例,所有对象会共用这个函数,那么函数内部如何区分是哪个对象调用自己
答案就是用this指针 this指针指向调用对象 (也就是说 this指针存储了调用对象的地址:当一个对象的成员函数被调用时,编译器会隐式地传递该对象的地址作为 this 指针)this指针的用途:解决形参和成员变量的命名冲突;返回对象本身,用于链式编程
空指针的访问
空指针可以访问成员函数;但是如果成员函数内部访问了成员,空指针就无法访问该成员函数
原因:任何对类成员的直接访问都被看作是 隐式引用了this指针,eg:当isbn类使用bookNo时,它隐式引用了this指针,就像我们书写了this->bookNo一样,所以空指针访问该函数会报错!
如果没有访问成员,例如函数里只有一句输出 hello world,那空指针访问完全没有问题;友元
友元作用 就是让一个函数或者类访问另一个类中私有成员;全局函数,类,成员函数都可以作友元;
具体用法和细节,就项目时知道怎么用就行;内联函数
注意几点即可- 内联函数作用就是 在编译阶段 直接该函数的代码副本放置在每个调用该函数的地方;
- 内联必须放在函数定义上,而不是声明上;
- 定义在类之中的成员函数将自动地成为内联函数 (一般成员函数我们采取类内声明,类外定义)
重载运算符
自定义类型的对象 运算符没法处理新对象,我们自己重载运算符封装一下功能;系统内置类型的对象,运算符已经封装好了功能,我们也可以重载但没有必要;
具体用法和细节,也是项目里知道怎么用就行;
5. 继承
- C++中的继承语法 class 子类 : 继承方式 父类; 另一种叫法:父类叫基类,子类叫派生类;
- 继承方式,三种继承方式 public protected private 区别主要就是子类非私有成员的访问权限;
如果是public继承,访问权限不变;如果是protected继承,访问权限变为protected;如果是private继承,访问权限变为private; - 菱形继承会导致,子类继承多份相同的数据,利用虚继承解决这个问题;如果B类虚继承A类,那么B类里只会存在一个vbptr,它指向vbtable,利用偏移量找到A类对象的内存;
6. 多态
- 多态的意义就是说 假如一个类 派生出了 很多子类;如果此时 用一个父类指针 指向了 子类对象;那么这个父类对象在调用函数时,会根据接受对象类型的不同,执行不同子类里的函数;
所以多态使用的场景很明显 我们需要有一个父类指针或引用 指向 子类对象
使用条件:继承关系,子类重写父类中的虚函数 - 函数重写 只发生在 继承的场景里;函数重写意味着 子类重写父类中的函数,然后 返回值,名字,参数列表完全一致;而函数重载只需要不同函数的函数名相同;
- 多态中,父类里的虚函数的实现无意义,我们只关心子类中重写的函数,所以我们可以将父类中的虚函数声明成纯虚函数;
语法:virtual 返回值类型 函数名 (参数列表)= 0;
有纯虚函数的类,就是抽象类,抽象类顾名思义无法实例化对象,然后子类必须重新纯虚函数,否则也会成为抽象类; - 虚析构 主要是解决 多态时delete父类指针,无法析构子类对象的场景;所以利用虚析构,就可以在delete父类指针时,也调用子类的析构代码;
纯虚析构,就是可以直接让该类成为抽象类;
所以虚析构和纯虚析构,主要就是解决子类堆区数据的释放问题;因为子类不在堆区的数据会被自动释放掉; - 多态的使用思想;
不用多态:我们需要个计算器对象,它需要有加减乘除功能;我们可以定义一个计算器类,然后里面定义加减乘除四个函数;
用多态:将功能分类 然后拆分成 不能的类;例如一个计算器,我们可以先定义一个抽象计算器类,然后再利用多态,定义加法计算器类,减法计算器类,乘法计算器类,除法计算器类;
多态的好处显而易见:降低耦合性,方便后期维护扩展;
7. 文件操作
- 为什么需要文件操作,我们程序里的数据在程序结束后就会销毁,为了可持续化保存,我们将程序里的数据存储在文件里;
- C++文件操作功能,主要用fstream库即可
- 非二进制读写
- 写:#include
fstream fout; fout.open(“file path”, param); fout << “data”; fout.close(); - 读:#include
fstream fin; fin.open(“file path”, param); loop read; fin.close();
- 写:#include
- 二进制读写,就是利用read,write函数;然后open的时候param要加上ios::binary
- 也可以不写open函数,直接在创建流对象的时候,调用构造函数;例如fstream fout(“file path”, param)