C++ 学习笔记
C和C++
设计思想上:
C++是面向对象的语言,而C是面向过程的结构化编程语言
语法上:
C++具有重载、继承和多态三种特性(封装使代码模块化,继承扩展已存在的代码,多态的目的是为了接口重用)
C++相比C,增加多许多类型安全的功能,比如强制类型转换、
C++支持范式编程,比如模板类、函数模板等
多态
https://www.cnblogs.com/ymjyqsx/p/9703005.html
C++ 支持两种多态性:编译时多态性,运行时多态性
编译时多态性(静态多态):通过重载函数实现
运行时多态性(动态多态):通过虚函数实现
实现多态的方式:函数重载、模板函数、虚函数
虚函数:
允许被其子类重新定义的成员函数,子类重新定义父类虚函数的做法,可实现成员函数的动态覆盖。
纯虚函数: 是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”。(包含纯虚函数的类称为抽象类。因其包含了没有定义的纯虚函数,所以不能进行实例化)
virtual void funtion()=0
构造函数不能为虚函数
1.虚函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。 要创建一个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数。
2.虚函数对应一张虚函数表,这个虚函数表是存储在对象的内存空间的,如果构造函数是虚函数就需要通过虚函数表来调用,可是对象还没有实例化,也就是内存空间还没有,找不到虚函数表,所以构造函数是不能声明为虚函数的。
析构函数可以为虚函数
将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。如果不为虚函数,只释放基类,不释放子类,造成内存泄漏。
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
函数重载
允许有不同参数的函数有相同的名字
void test(int arg){}
void test(char arg){}
void test(int arg1,int arg2){}
模板函数
函数内容确定,但函数的参数类型却是待定的(注意:参数个数不是待定的)
template < typename T>
T getMax(T arg1, T arg2)
{
return arg1 > arg2 ? arg1:arg2; //代码段1
}
char* getMax(char* arg1, char* arg2)
{
return (strcmp(arg1, arg2) > 0)?arg1:arg2;//代码段2
}
函数模板跟普通函数一样,也可以被重载
C++编译器优先考虑普通函数
如果函数模板可以产生一个更好的匹配,那么就选择函数模板
也可通过空模板实参列表<>限定编译器只匹配函数模板(fun<>(a, b);)
static
全局静态变量
内存中位置:静态存储区,在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化)
作用域:静态全变量在声明它的整个文件都是可见的,而在文件之外是不可见的;静态变 量都在全局数据区分配内存,包括后面将要提到的静态局部变量全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
局部静态变量
内存中的位置:静态存储区
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化) ;
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变。
静态函数
静态函数不能被其它文件所用;
其它文件中可以啶义相同名字的函数,不会发生冲突;
类的静态成员
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。
类的静态函数
对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新。
const
const修饰变量
变量的值不能改变
const修饰指针
如果const位于\(*\)的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量
如果const位于\(*\)的右侧,则const就是修饰指针本身,即指针本身是常量
指针常量:不能通过指针来修改变量的值。
常量指针:一直指向该变量,不能给该指针赋予其他地址
函数中使用const
const修饰函数参数:表示参数不可变;若参数为引用,可以增加效率
const 修饰函数返回值:const来修饰返回的指针或引用,保护指针指向的内容或引用的内容不被修改,也常用于运算符重载。归根究底就是使得函数调用表达式不能作为左值。
类中使用const
const修饰成员变量:表示成员变量不能被修改,同时只能在初始化列表中赋值
const修饰成员函数:该函数不能改变对象的成员变量;不能调用非const成员函数,因为任何非const成员函数会有修改成员变量的企图;const的成员函数才能被一个const类对象调用。即const类对象只能调用const成员函数const关键字不能与static关键字同时使用,因为static关键字修饰静态成员函数,静态成员函数不含有this指针,即不能实例化,const成员函数必须具体到某一实例。(析构函数和静态成员函数不能声明为const)
const修饰类对象:对象的任何成员都不能被修改;只能调用const成员函数
指针和引用
1.指针有自己的一块空间,而引用只是一个别名:
2.使用sizeof看一一个指针的大小是4,而引用则是被引用对象的大小;
3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会
改变引用所指向的对象;
5.可以有const指针,但是没有const引用;
6.指针在使用中可以指向其它对象,但是引用只能是一一个对象的引用,不能被改变;
7.指针可以有多级指针(**p) ,而引用至于一-级;
8.指针和引用使用++运算符的意义不一样;
9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
类
访问控制
关键字private和public描述了对类成员的访问控制。使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数(或友元函数)来访问对象的私有成员。防止程序直接访间数据被称为数据隐藏。C++还提供了第三个访问控制关键字protected。
对象
所创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法,即每种方法只有一个副本。在OOP中,调用成员函数被称为发送消息,因此将同样的消息发送给两个不同的对象将调用同一个方法,但该方法被用于两个不同的对象。
析构函数
析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,例如 ~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。
如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。
如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。
类析构顺序: 1)派生类本身的析构函数: 2)对象成员析构函数: 3)基类析构函数。
智能指针
作用
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
auto_ptr(c++11弃用)
采用所有权模式
auto_ptr <string> p1 (new string ("I reigned lonely as a cloud."));
auto_ptr <string> p2;
p2 = p1; //auto_ptr不会报错.
此时不会报错,但当程序访问p1时将会报错。(存在潜在的内存崩溃问题)
unique_ptr
不允许多个指针共享资源,可以使用标准库中的move函数转移指针
unique_ptr <string> pl (new string ("I reigned lonely as a cloud."));
unique_ptr <string> p2;
p2 = pl; //报错.
当程序试图将一个unique_ptr赋值给另一个时,如果原unique_ptr是个临时右值,编译器允许这么做;如果原unique_ ptr将存在一段时间,编译器将禁止这么做
unique_ ptr<string> pu3;
pu3 = unique_ptr<string> (new string (" You")) ; //allowed
shared_ptr
多个指针共享资源。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁“时候释放。它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count ()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ ptr, unique_ ptr, weak _ptr 来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
当两个对象相互使用一个shared_ ptr 成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
weak_ptr
weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象。进行该对象的内存管理的是那个强引用的shared_ptr。weak_ptr 只是提供了对管理对象的一个访问手段。它只可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用记数的增加或减少。
weak_ptr是用来解决share_dptr相互引用时的死锁问题,如果说两个shared_ptr 相互引用,那么这两个指针的引用计数永远不会下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ ptr 之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ ptr。
new和malloc
https://www.cnblogs.com/QG-whz/p/5140930.html#_label1_0
1. 申请的内存所在位置
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。
2.返回类型安全性
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
3.内存分配失败时的返回值
new内存分配失败时,会抛出bad_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
4.是否需要指定内存大小
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
5.是否调用构造函数/析构函数
使用new操作符来分配对象内存时会经历三个步骤:
- 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
- 第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
- 第三部:对象构造完成后,返回一个指向该对象的指针。
使用delete操作符来释放对象内存时会经历两个步骤:
- 第一步:调用对象的析构函数。
- 第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。
总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会。
6.对数组的处理
C++提供了new[]与delete[]来专门处理数组类型:
A * ptr = new A[10];//分配10个A对象
使用new[]分配的内存必须使用delete[]进行释放:
delete [] ptr;
new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数。注意delete[]要与new[]配套使用,不然会找出数组对象部分释放的现象,造成内存泄漏。
至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:
int * ptr = (int *) malloc( sizeof(int)* 10 );//分配一个10个int元素的数组
特征 | new/delete | malloc/free |
---|---|---|
分配内存的位置 | 自由存储区 | 堆 |
内存分配成功的返回值 | 完整类型指针 | void* |
内存分配失败的返回值 | 默认抛出异常 | 返回NULL |
分配内存的大小 | 由编译器根据类型计算得出 | 必须显式指定字节数 |
处理数组 | 有处理数组的new版本new[] | 需要用户计算数组的大小后进行内存分配 |
已分配内存的扩充 | 无法直观地处理 | 使用realloc简单完成 |
是否相互调用 | 可以,看具体的operator new/delete实现 | 不可调用new |
分配内存时内存不足 | 客户能够指定处理函数或重新制定分配器 | 无法通过用户代码进行处理 |
函数重载 | 允许 | 不允许 |
构造函数与析构函数 | 调用 | 不调用 |
内存管理
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
bss段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
栈区:使用栈空间存储函数的返回地址、参数、局部变量、返回值
映射区:存储动态链接库以及调用mmap函数进行的文件映射