C++与C
c++是面向对象的语言,c是面向过程的语言。
语法上:
C++增加了封装、多态、继承三种特性
C++相比C增加许多类型安全的功能,如强制类型转换
C++支持范式编程:如模板类、模板函数
一、类
1.类访问修饰符public、private、protected
(1)修饰成员
公有成员public: 公有成员在类内外都可访问。
私有成员private: 私有成员或函数在类的外部是不可访问的,甚至是不可查看的。只有类内和友元函数可以访问私有成员,不能被派生类访问。默认情况下,类的所有成员都为私有。
在类外通过公有成员函数访问私有成员,通过临时对象的桥梁改变、获取私有成员的值。
保护成员protected: 与私有成员不同的是保护成员可在派生类中访问。
(2)继承方式
public继承(不改变基类成员访问属性): 基类public成员,protected成员的访问属性在派生类中分别变成:public, protected
protected继承:(public访问限制为protected) 基类public成员,protected成员的访问属性在派生类中分别变成:protected,protected
private继承:(public、protected限制为private) 基类 public 成员,protected 成员的访问属性在派生类中分别变成:private, private
基类private成员始终不能对派生类可见
类默认采用private继承,struct默认public继承
2.构造&析构函数
构造函数不返回任何类型,也不返回void。用于分配空间,初始化成员。
使用初始化列表来初始化字段时是按照变量声明的顺序初始化,而不是按照出现在初始化列表中的顺序。有时需注意按声明顺序罗列,否则可能会有问题,如下情况:
int a;
int b;//先声明了a后声明了b
OneClass(int i):b(i),a(b){}//由于a先声明,a先初始化,被初始化为一个不可预测的值
析构函数,删除对象时执行。不返回任何值,不带任何参数。一般用于在跳出程序时释放资源。
初始化列表(冒号语法)
用于构造函数后。表示构造该类对象时对其成员变量构造函数进行调用。对于普通数据成员,则相当于赋值运算。常量要在初始化列表初始化。
3.拷贝构造函数
创建对象时使用同一类之前创建的对象初始化新创建的对象。
必须定义拷贝构造函数的情况
如果没有定义拷贝构造函数,系统会自行生成一个。如果类带有指针成员变量,或者有成员动态内存分布,则必须自定义。
- 释放已有内存,防止内存泄露
- 重新生成新内存深拷贝
原因: 默认的拷贝构造函数实现的只能是浅拷贝,即直接将原对象的数据成员值依次复制给新对象中对应的数据成员,并没有为新对象另外分配内存资源。
这样,如果对象的数据成员是指针,两个指针对象实际上指向的是同一块内存空间。 在某些情况下,浅拷贝回带来数据安全方面的隐患。
当类的数据成员中有指针类型时,我们就必须定义一个特定的拷贝构造函数,该拷贝构造函数不仅可以实现原对象和新对象之间数据成员的拷贝,而且可以为新的对象分配单独的内存资源,这就是深拷贝构造函数。
复制构造函数使用常量引用作为参数。
classname(const classname& obj){}
若采用传值的方式,则复制构造函数本身也会调用复制构造函数,从而进入无限递归导致栈溢出。赋值类(如重载“=”)均使用常量引用,以避免
调用场景:
- 对象以值传递的方式传入函数
- 对象以值传递的方式从函数返回
- 一个对象使用另外一个对象初始化(两种初始化形式:括号、‘=’)
4.友元
类的友元声明在类内部,定义在类外部,有权访问类的所有私有成员和保护成员。 虽然友元的原型在类中出现,但是友元函数并不是类的成员函数。
友元可以是一个函数也可是类,友元类整个类及其成员都是友元。
声明一个函数(类)是一个类的友元,需在类中定义该函数原型前加关键字friend:
class classname{
public:
friend void fun(classname obj);//友元函数
friend class classfriend;//友元类
}
友元函数使用:
- 访问非static成员,需要对象做参数(因为要访问具体对象的私有成员)
- 访问static或全局变量,不需要对象做参数(所有对象共享)
- 友元函数直接调用,不需要通过对象或指针
5.类成员变量初始化
最常用:static(const)在类外,其余在初始化列表,const不能在构造函数内
C++11 前
普通变量在构造函数内或者初始化列表初始化。const类型成员智能在初始化列表初始化。static静态成员变量只能在类外初始化。static const静态常量只有整型可在类内初始化,其他都只能在类外初始化。
type | normal | const | static | static const |
---|---|---|---|---|
声明时初始化 | × | × | × | ×(只有静态常量整形可以) |
初始化列表初始化 | √ | √ | × | × |
构造函数内初始化 | √ | × | × | × |
类外初始化 | × | × | √ | √ |
C++11后
普通变量可在类内、构造函数内、初始化列表初始化。const成员可在类内、初始化列表初始化。static静态成员只能在类外初始化。static const静态常量只有整型能在类内初始化,其他都只能在类外初始化。
type | normal | const | static | static const |
---|---|---|---|---|
声明时初始化 | √ | √ | × | ×(只有静态常量整形可以) |
初始化列表初始化 | √ | √ | × | × |
构造函数内初始化 | √ | × | × | × |
类外初始化 | × | × | √ | √ |
6.继承
依据一个类定义另一个类,重用代码。
一个类可以派生自多个类,使用类派生列表:
class derived-class:access-specificer base-class1,access-specificer base-class2 ...
其中access-specifer为访问修饰符:public、private、protected中的一个,默认为private。base-class是某个基类名称。
派生类继承了类的所有方法,除构造、析构拷贝构造函数,重载运算符,友元函数。
7.重载
同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
函数重载: 同名函数,形参(个数、类型或顺序)必须不同(与返回值无关)。
运算符重载:
Type operator+(const Type&);//类成员
Type operator+(const Type&,const Type&)//非类成员
两个函数一个带const一个不带const也是重载。
覆盖(重写)【多态】、隐藏(重定义)【多态】、重载
(1)覆盖(重写)
发生在基类和子类
子类重新定义父类虚函数的做法称为覆盖(override),或者称为重写。
重写从基类继承过来的虚函数:被重写的函数必须是virtual的。函数名,返回值、参数列表都必须和基类的相同。
通过类指针调用时:看指针指向的具体内容,而不是指针的类型。需同过虚函数表找到虚函数的地址,而虚函数表存放在每个对象中(依赖于对象内容而不是类型)。不在编译期间实现,只能在运行时绑定,是一种动态绑定。
如使用父类指针指向了一个子类对象,指针类型是父类指针,指针内容是子类(子类对象所在的地址)。若通过指针调用被覆盖的函数(父类的virtual函数),即调用子类中对函数的重写。
这种情况为动态多态(动态链接):在派生类中重新定义基类中的虚函数时,会告诉编译器不要静态链接到该函数。在程序中任意点可以根据所调用的对象类型选择调用的函数,称为后期绑定。
(2)隐藏(重定义)
发生在基类和子类
子类重新定义父类中具有相同名称的非虚函数(参数表也可以不同),此时基类中的函数被隐藏。
- 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(有virtual由于参数不同做非虚函数处理,
- 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
通过类指针调用时:看指针原型(指针类型),而不是指针的内容:此过程不会涉及到对象的内容,只会涉及对象的类型,编译时绑定,是一种静态绑定。
如使用父类指针指向了一个子类对象,指针类型是父类指针,指针内容是子类(子类对象所在的地址)。若通过指针调用非虚函数且子类中也有同名函数(只有参数相同时会发生歧义),即调用父类中该函数。
这种情况为静态链接:函数调用在程序执行前编译期间就设置好了,称为早绑定。
(3)重载
在一个类的定义下,不涉及继承
是允许有多个同名的函数,而这些函数的参数列表必须不同(类型、个数、顺序)。编译器会根据这些函数的不同列表,将同名的函数的名称做修饰,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。但这并没有体现多态性。
8.多态
(1)形成多态必须存在继承关系
当类之间存在层次结构,且类是通过继承关联,就会用到多态,在每一个子类中对同名函数的多种实现。
在调用成员函数时,会根据调用对象的类型来执行不同的函数。
(2)动态联编
虚函数的动态联编机制打破了静态编译,使得可以通过基类指针访问子类定义的函数,而非函数在编译时就固定绑定。虚函数看指针指向的对象,非虚函数看指针类型:
1,如果以一个基类指针指向一个派生类对象,那么经由该指针访问非虚函数只能访问基类定义的函数(静态联编)
2,如果以一个派生类指针指向一个基类对象,必须先做强制类型转换(explicit cast).此时非虚函数调用为派生类,虚函数调用基类.
3,如果基类和派生类定义了相同名称的非虚成员函数(包括参数相同),那么通过对象指针调用非虚成员函数时,到底调用那个函数要根据指针的类型来确定,而不是根据指针实际指向的对象类型确定。
动态联编的实现机制 VTABLE
编译器为每个包含虚函数的类创建虚函数表,子类在继承基类虚函数表的基础上随着重载更新表中项。包含虚函数表的类创建对象时,编译器会在每个对象中增加vtbl指针指向对应类的虚函数表,从而指定其所使用的虚函数,调用时通过获取该vtbl指针查找对应函数进行调用。
编译器对每个包含虚函数的类创建一个虚函数表VTABLE,表中每一项指向一个虚函数的地址,即VTABLE表可以看成一个函数指针的数组,每个虚函数的入口地址就是这个数组的一个元素。
每个含有虚函数的类都有各自的一张虚函数表VTABLE。每个派生类的VTABLE继承了它各个基类的VTABLE,如果基类VTABLE中包含某一项(虚函数的入口地址),则其派生类的VTABLE中也将包含同样的一项,但是两项的值可能不同。如果派生类中重载了该项对应的虚函数,则派生类VTABLE的该项指向重载后的虚函数,如果派生类中没有对该项对应的虚函数进行重新定义,则使用基类的这个虚函数地址。
在创建含有虚函数的类的对象的时候,编译器会在每个对象的内存布局中增加一个vtbl指针项,该指针指向本类的VTABLE。在通过指向基类对象的指针(设为bp)调用一个虚函数时,编译器生成的代码是先获取所指对象的vtbl指针,然后调用vtbl所指向类的VTABLE中的对应项(具体虚函数的入口地址)。
当基类中没有定义虚函数时,其长度=数据成员长度;派生类长度=自身数据成员长度+基类继承的数据成员长度;
当基类中定义虚函数后,其长度=数据成员长度+虚函数表的地址长度;派生类长度=自身数据成员长度+基类继承的数据成员长度+虚函数表的地址长度。
包含一个虚函数和几个虚函数的类对象空间长度增量为0。含有虚函数的类只是增加了一个指针用于存储虚函数表的首地址。
派生类与基类同名的虚函数在VTABLE中有相同的索引号(或序号)。
(3)纯虚函数:
virtual void func()=0;
纯虚函数不许定义其具体动作,它的存在只是为了在衍生类钟被重新定义。只要是拥有纯虚拟函数的类,就是抽象类,它们是不能够被实例化的(只能被继承)。如果一个继承类没有重写父类中的纯虚函数,那么他也是抽象类,也不能被实例化。
抽象类不能被实例化,不过我们可以拥有指向抽象类的指针,以便于操纵各个衍生类。
虚函数衍生下去仍然是虚函数,而且还可以省略掉关键字“virtual”。
(4)虚函数的重载性与权限无关
虚函数可以为private,并且可以被子类覆盖,但子类不能调用父类private虚函数。虚函数的重载性与声明它的权限无关。
二、const关键字
1.const T、const T*、T* const、const T&、const T*&、T* const&的区别
(1)const T
定义一个常量。声明的同时必须初始化。一旦声明,这个值将不能被改变。
(2)const T *
指向常量的指针,不能用于改变所指对象的值。
允许指向常量的指针指向非常量,不可通过该指针改变所指对象。
(3)T* const
常量指针,声明时必须初始化。指针本身是常量,不可修改,但可修改其所指对象的内容。
const T* const 指向常量的常量指针。既不可以改变指针本身的值,也不可以改变指针所指的对象。
(4)const T &(==T const&)
对常量的引用,又称为常量引用,常量引用不可修改其绑定的对象。
允许常量引用绑定非常量对象。不可通过该引用修改其绑定对象。
(5)const T * &与T* const &
const T*&:指向常量的指针的引用。可改变指针,不可改变指向的对象。
T* const&:常量指针的引用。不可改变指针本身,可改变其指向对象。
2.const成员函数、const成员变量
成员函数形参列表后带const,这样的成员函数叫做类的const成员函数,也叫常量成员函数。 这个const作用是用来修改隐式this指针的类型,把它变成 指向常量(该类常量)的常量指针
const ClassName* const this = sth;// const T* const
常量函数只能读取数据成员,不能改变调用它的对象的内容,即不能修改对象的成员变量.非常量对象可以调用常量成员函数,但不能通过常量成员函数修改对象的内容.
如果声明了常量对象(常量对象的引用和指向常量对象的指针), 不能调用非常量函数,只能调用常量成员函数
作为一种良好的编程风格,在声明一个成员函数时,若该成员函数并不对数据成员进行修改, 应尽可能将该成员函数声明为const成员函数
注意:const成员函数可以被对应的具有相同形参列表的非const成员函数重载
当const版本和非const版本的成员函数同时出现时(const版本被具有相同形参列表的非const版本重载),非const对象调用非const成员函数。
类中的const成员变量:
- 放在初始化列表初始化
- 声明时初始化
三、static关键字
1.全局变量、局部变量、静态全局变量、静态局部变量
变量作用域共分为6种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。
(1)从作用域区分:
全局变量有全局作用域,只需在一个源文件中定义,就可作用于所有的源文件。其他不包含全局变量定义的源文件需要extern关键字再次声明这个全局变量。
静态局部变量(函数中定义的静态变量)具有局部作用域。只被初始化一次,第一次初始化直到程序结束一直存在,只对定义自己的函数体始终可见。
未经初始化的静态变量会被自动初始化为0,普通对象则是任意初始化的。
局部变量也只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
静态全局变量具有文件作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,只能为该源文件内的函数公用,不能作用到其它文件里。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
(2)从内存分配区分:
全局变量,静态局部变量,静态全局变量都在静态存储区分配空间(下一次调用保持原来赋值),而局部变量在堆栈里分配空间。static 全局变量:改变作用范围,将全局变量的全局作用域指定为文件作用域,不改变存储位置;static 局部变量:改变局部变量推展存储至静态存储区存储,不改变其作用范围。
设计:全局变量若仅被一个文件访问,改为静态全局变量,以降低文件间耦合度。全局变量若仅由单个函数访问,改位静态局部变量以降低模块耦合度。
函数需返回函数内定义变量指针或引用时,需定义静态局部变量。
2.类内静态成员变量
类内成员被声明为static,意味着被该类所有的对象共享,需在类外初始化(类内仅声明)(除static const int)。类的静态成员函数没有this指针,仅能访问类的静态成员。
static int getstaticvalue(){};//类内定义静态成员函数
classname::getstaticvalue();//类外使用
3.静态函数
在函数返回值前加static,函数被定义为静态函数。静态函数仅在声明他的文件中可见,不能被其他文件所用。函数的实现使用static修饰,则只能在本cpp内使用(注意不要在头文件中声明静态全局函数,非static全局函数需要在头文件声明)
四、Lambda表达式
lambda表达式把函数看做对象。可以像对象一样使用,比如可以赋给变量和作为参数传递,还可以像函数一样对其求值。
1.[capture]:捕捉列表。
捕捉列表总是出现在 lambda 表达式的开始处。事实上,[] 是 lambda 引出符。编译器根据该引出符判断接下来的代码是否是 lambda 函数。捕捉列表能够捕捉上下文中的变量供 lambda 函数使用。
2.->return_type:返回类型。
用追踪返回类型形式声明函数的返回类型。出于方便,不需要返回值的时候也可以连同符号 -> 一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
3.{statement}:函数体。
内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
[capture](parameters)->return-type{body}
例:[](int x,int y)->int{int z=x+y;return z+x;}//指定返回类型
auto fun=[](int x,int y)->int{int z=x+y;return z+x;};
fun(x,y);
在lambda表达式内可以访问当前作用域的变量,这是其闭包(closure)行为。
传值与传引用由[ ]指定:
[]//不捕获任何变量,不能使用参数表以外的变量
[x,&y]//x以传值方式捕获,y以引用方式捕获(指定变量)
[&]//任何被使用到的外部变量都以引用的形式传入
[=]//任何被使用到的外部变量都以传值的方式传入
[&,x]//x显示地以传值方式传入。其余变量以引用方式传入
[=,&z]//z以引用的方式传入。其余变量以传值的方式传入
this指针可以使用[=][&]默认全部传入,或者显式地使用[this]
五、异常处理
throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
catch: 在您想要处理问题的地方,通过异常处理程序捕获throw抛出的异常。catch 关键字用于捕获异常。
try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
try
{
// 保护代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}
六、多线程
(1)使用thread创建线程
#include <thread>
#include <iostream>
void fun(int a){
a++;
std::cout<<a<<std::endl;
}
int main(){
int a=0;
std::thread t(fun,a);
t.join();
}
调用thread类,并利用了thread的构造函数创建一个线程t,对于非类成员函数,构造函数参数为(函数地址,函数参数)。创造线程时运行线程。
(2)join()和detach()。
- join()函数是启动子线程而阻塞主线程,当子线程运行结束后,才会继续运行主线程。
- detach()函数的作用是启动子线程,并且让子线程和主线程分离,子线程和主线程各运行各的,虽然两个线程会因为共享内存池的原因在操作系统的层面发生发生阻塞等关系,但是在代码层次上,两个线程并不存在谁阻塞谁,很可能主线程已经运行结束了,子线程还在运行。
(3)类成员函数初始化thread类的构造函数
对于类的成员函数,需要给出对象的地址
#include<iostream>
#include<thread>
class A{
public:
void fun(int a,int b){
std::cout<<"this is A thread!"<<a<<std::endl;
}
};
int main(){
int k=0;
A a;
std::thread t(&A::fun,a,k,k+1);
t.join();
}
std::thread t(函数(成员函数)地址,对象地址,成员函数的参数1,参数2,参数3…)
相比非成员函数,成员函数需要给出类实例化对象的地址,如果该线程是在同一类的某一成员函数当中被构造,则直接用this关键字代替即可。
(4)加锁解锁
每一个线程只要在一个进程内,都是共享内存池的,这样在读写数据可能会发生混乱。C++11提供了mutex类进行加锁和解锁。
#include<iostream>
#include<thread>
#include<mutex>
std::mutex mut;
class A{
public:
volatile int temp;
A(){
temp=0;
}
void fun(int num){
int count=10;
while(count>0){
mut.lock();
temp++;
std::cout<<"thread_"<<num<<"...temp="<<temp<<std::endl;
mut.unlock();
count--;
}
}
void thread_run(){
std::thread t1(&A::fun,this,1);
std::thread t2(&A::fun,this,2);
t1.join();
t2.join();
}
};
int main(){
A a;
a.thread_run();
}
然后,我们说一下volatile关键字。
volatile和const关键很相似,都是修饰变量的,只是二者功能不一样。
volatile在多线程当中经常使用,因为在某一线程多次调用某一个变量,编译器会进行优化,将该变量存放在在寄存器当中,不会每次都从内存当中读入。果然该变量同时在其他线程当中被修改,这样就会发生脏读取错误。
而加上volatile修饰,则会提醒编译器,这个变量可能会被改变,不能存放到寄存器当中,需要每次都从内存当中读取。
进程与线程
(1)定义:
进程: 具有一定独立功能的程序关于某个数据集合上的一次运行活动。进程是进行资源分配和调度的一个单位。
线程: 进程的一个实体。cpu分配和调度的基本单位,比进程更小的能独立运行的基本单位。不拥有系统资源,仅拥有自己的一点运行中必不可少的资源,可与同属于一个进程的其他线程共享进程所拥有的全部资源。
(2)关系
一个线程可以撤销创建另一个线程。同一个进程中的多个线程之间可以并发执行。
线程拥有自己的栈空间,拥有自己独立的执行队列。
(3)区别
进程有独立的地址空间,崩溃后不影响其他进程。线程只是一个进程中的不同执行路径,没有单独的地址空间。多进程更加鲁棒。但进程切换耗费资源更大。对于要求同时进行并且共享某些变量的。只能用线程,不能用进程。进程有独立的内存单元。而多线程共享内存单元。
(4)优缺点
线程执行开销小,但不利于资源管理维护。
进程相反。且进程可以跨机器。
七、模板
定义
//函数模板
template<class type>
return-type func-name(parameter list){
// 函数主体
}
//类模板
template <class type1,class type2>
class class_name{
// 类主体
}
其中type、type1、type2 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。
八、智能指针
auto_ptr,shared_ptr,weak_ptr,unique_ptr,后三个是C++11支持,第一个已被C++11弃用.
1.为什么要使用智能指针:
智能指针的作用是管理一个指针。当用户存在申请的空间在函数结束时忘记释放的情况,即内存泄露。使用智能指针可以很大程度上避免这个问题。智能指针是一个类,当超出了类的作用范围,自动调用析构函数释放资源。同时该类又有与指针相同的表现。
2.四种智能指针
(1)auto_ptr
采用所有权模式,独占式拥有
只有一个auto_ptr对象拥有某指针的所有权。赋值时即给出所有。
auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.
当忘记所有权已经给出时再访问(赋值后访问上述p1),报错:存在潜在的内存崩溃问题。
(2)unique_ptr(代替auto_ptr)
unique_ptr独占式拥有。同一时间内仅有一个智能指针拥有对某指针的所有权。当一个unique_ptr指向某指针,则不可以交出给其他unique_ptr对象。
unique_ptr<string> p3 (new string ("auto")); //#4
unique_ptr<string> p4; //#5
p4 = p3;//此时会报错!!
允许通过临时对象赋值
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));
需要安全的重用该种指针,给它赋新值,可以使用move
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
(3)shared_ptr
shared_ptr实现共享式拥有。可以使用多个该智能指针指向同一个对象。该对象和相关资源会在最后一个引用被销毁时释放。使用计数机制,赋值构造时计数++。release()释放当前所有权,计数-1。当计数等于0时,资源被释放。
成员函数:
use_count返回计数引用的个数
unique返回是否是独占所有权(use_count == 1)
swap交换两个shared_ptr对象(即交换所拥有的对象)
reset放弃内部对象的所有权或拥有对象的变更
get()返回内部对象指针。sp==sp.get()
(4)weak_ptr
weak_ptr是一种不控制对象生命周期的智能指针 ,指向一个shared_ptr管理的对象。进行该对象内存管理的是shared_ptr,weak_ptr只是提供了对管理对象的一个访问手段。构造和析构不会引起计数器的增加或减少。主要用于解决shared_ptr相互引用时的死锁问题。 两个shared_ptr相互引用,那么两个指针的引用计数永远不会为0,导致永远不会被释放。
不能通过weak_ptr访问对象的方法,仅是用于代替shared_ptr防止相互引用。
3.实现一个带有引用计数的智能指针
明确:
智能指针是一个类,使用构造函数和析构函数对计数及内存释放进行维护。
表现出指针的行为。
对任何类型可用,因此是一个模板。
template<class T>
class SharedPointer
{
public:
//默认构造函数,内部指针,未指向任何资源,引用计数为0,因为它未与任何资源绑定
SharedPointer() :m_refCount(nullptr), m_pointer(nullptr){}
//构造函数,初始化时,指向一个已经分配好的资源
SharedPointer(T* adoptTarget) :m_refCount(nullptr), m_pointer(adoptTarget)
{
addReference();
}
//构造函数,使用其它对象创建新对象
SharedPointer(const SharedPointer<T>& copy)
:m_refCount(copy.m_refCount), m_pointer(copy.m_pointer)
{
addReference();
}
//析构函数,引用计数递减,当为0时,释放资源
virtual ~SharedPointer()
{
removeReference();
}
//赋值操作
//当左值被赋值时,表明它不再指向所指的资源,故引用计数减一
//之后,它指向了新的资源,所以对应这个资源的引用计数加一
SharedPointer<T>& operator=(const SharedPointer<T>& that)
{
if (this != &that)
{
removeReference();
this->m_pointer = that.m_pointer;
this->m_refCount = that.m_refCount;
addReference();
}
return *this;
}
//判断是否指向同一个资源
bool operator==(const SharedPointer<T>& other)
{
return m_pointer == other.m_pointer;
}
bool operator!=(const SharedPointer<T>& other)
{
return !operator==(other);
}
//指针解引用
T& operator*() const
{
return *m_pointer;
}
//调用所知对象的公共成员
T* operator->() const
{
return m_pointer;
}
//获取引用计数个数
int GetReferenceCount() const
{
if (m_refCount)
{
return *m_refCount;
}
else
{
return -1;
}
}
protected:
//当为nullpter时,创建引用计数资源,并初始化为1
//否则,引用计数加1。
void addReference()
{
if (m_refCount)
{
(*m_refCount)++;
}
else
{
m_refCount = new int(0);
*m_refCount = 1;
}
}
//引用计数减一,当变为0时,释放所有资源
void removeReference()
{
if (m_refCount)
{
(*m_refCount)--;
if (*m_refCount == 0)
{
delete m_refCount;
delete m_pointer;
m_refCount = 0;
m_pointer = 0;
}
}
}
private:
int * m_refCount;
T * m_pointer;
};
九、C++计时器
#include <ctime>
#include <cstdlib>
#include <chrono> //时间库
class TicToc{
public:
TicToc(){tic();}
void tic(){
start=std::chrono::system_clock::now();
}
double toc(){
end=std::chrono::system_clock::now();
std::chrono::duration<double> mseconds=end-start;
return mseconds.count()*1000;
}
private:
std::chrono::time_point<std::chrono::system_clock> start,end;
}
十、其他
1.关键字相关
(1) dynamic_cast<type_id>(expression) 基类子类带有安全检查的类型转换
动态类型转换,只能用于含有虚函数的类,只能转指针和引用。 用于把expression转换成type_id对象。Type_id为类的指针、类的引用和void*类型。用于上行转换:将子类转为父类;下行转换:将父类转为子类,此时父类需有虚函数,会进行安全检查,比static_cast安全。dynamic_cast会根据基类指针是否真正指向继承类进行判断。
(2) const_cast<type_id>(expression) 修改对象const属性
去除const属性
(3) export 访问其他单元
用于访问其他编译单元(如另一代码文件)中的变量或对象,对于普通类型(基本数据类、结构),可以利用关键字extern,来使用这些变量或对象。对于模板类性,必须在定义这些模板类对象和模板函数是使用export导出。
(4) extern 声明全局变量
声明外部变量或函数,提供一个全局变量的引用。即该变量或函数名在其他文件中可见。用其声明的变量或者函数应该在别的文件或同一文件的其他地方定义(实现)。普通情况下定义即是声明,extern声明不是定义。extern告诉编译器该变量在其他地方定义了。如果声明有初始化式(赋值),无论前面有没有extern,都被当做定义。
(5) static_cast<type_id>(expression) 无安全检查的类型转换
隐式转换。 把expression转化为type_id类型,无安全性检查:
- 用于基类子类之间指针引用转换。上行转换是安全的,下行转换由于没有动态类型检查,是不安全的。
- 用于基本数据类型的转换。安全性由开发人员保证
- 把空指针转换为目标类型的空指针
- 非const转const
(6) default switch语句中表示其他情况
用在switch语句中,表示所有的case都不成立
不能转换掉const、volatile属性
(7) typedef 类型 定义名; 重命名类型
2.数据类型占用空间
char 1字节 8位
int 4字节
short 2字节
long 8字节
float 4字节
double 8字节
地址 32位系统 4字节 64位系统 8字节
使用sizeof()运算符查看:
sizeof()
3.枚举类型中间赋值,后面元素继续这个初值,前面元素仍从0开始。
enum color { red, green=5, blue };//red==0,blue==6
4.运算符优先级
(1)赋值运算符:
- 不能在‘=’左边写右值
- 连续使用“=”,右结合性:以 a = b = c 为例。由于赋值运算符是右结合性的,原表达式相当于 a = (b = c) ,所以先计算 b = c, 然后返回 b;那么表达式就变成了 a = b,a 的值变为 b 的值。最后的结果就是 a 和 b 的值都变成 c 的值了。——这是符合人的直观感受的。可以证明,对于超过两个连续使用的赋值运算符,其结果都与上面的类似。这样做是不会引发编译器或者程序员的误解的。
(2)连续使用“==”:
a == b == c
**左结合性:**原表达式相当于 (a == b) == c 先计算 a == b,返回 true 或者 false。由于 c 是 int 型,因此会发生隐式类型转换, true 或者 false 会被转换为 int 型的 1 或者 0。最后表达式就变成 1 == c 或者 0 == c。这显然不是在检查 a、b、c 三个变量是否相等,会引起误解。可以证明,对于超过两个连续使用的 == 运算符,都会引起类似的误解。
5.memcopy与memmove
void *memcpy(void *restrict s1, const void *restrict s2, size_t n);
void *memmove(void *s1, const void *s2, size_t n);
这两个函数都是将s2指向位置的n字节数据拷贝到s1指向的位置。
区别就在于关键字restrict, memcpy假定两块内存区域没有数据重叠,不会检查,而memmove没有这个前提条件。如果复制的两个区域存在重叠时使用memcpy,其结果是不可预知的,有可能成功也有可能失败的,所以如果使用了memcpy,程序员自身必须确保两块内存没有重叠部分
memmove会对拷贝的数据作检查,确保内存没有重叠,如果发现重叠会覆盖数据,简单的实现是调转开始拷贝的位置,从尾部开始拷贝:。
memcopy与strcpy
strcpy提供了字符串的复制。即strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符。
memcpy提供了一般内存的复制。即memcpy对于需要复制的内容没有限制,因此用途更广。
strcpy和memcpy主要有以下3方面的区别
1、复制的内容不同。 strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2、复制的方法不同。 strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3、用途不同。 通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
6.分配与释放空间(动态内存)
type *p;
p=new type(n);
delete []p;//释放p及所在数组中的每个元素
三维数组的分配释放
int ***array;//三维数组
// 假定数组第一维为 m, 第二维为 n, 第三维为h
// 动态分配空间
array = new int **[m];//分配第一维
for( int i=0; i<m; i++ )
{
array[i] = new int *[n];//分配第二维
for( int j=0; j<n; j++ )
{
array[i][j] = new int [h];//分配第三维
}
}
//释放(分配倒序)
for( int i=0; i<m; i++ )
{
for( int j=0; j<n; j++ )
{
delete[] array[i][j];//释放第三维
}
delete[] array[i];//释放第二维
}
delete[] array;//释放第一维
new与malloc的区别
首先,new/delete是C++的关键字,而malloc/free是C语言的库函数,后者使用必须指明申请内存空间的大小,对于类类型的对象,后者不会调用构造函数和析构函数
malloc需要给定申请内存的大小,返回的指针需要强转。
new会调用构造函数,不用指定内存大小,返回的指针不用强转。
内存泄漏
内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete。为了判断内存是否泄露,我们一方面可以使用linux环境下的内存泄漏检查工具Valgrind,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。
段错误
段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:
使用野指针
试图修改字符串常量的内容
7.引用
不存在空引用。
引用必须在创建时初始化。
一旦引用被初始化为一个对象,就不能指向另外一个对象。
只有函数返回引用时,才可以作为左值。(连续赋值)
double& setValues( int i )
{
return vals[i]; // 返回第 i 个元素的引用
}
setValues(1) = 20.23; //作为左值
类内重载‘=’赋值运算返回引用
//返回引用以允许连续赋值
classname& classname::operator=(const classname& obj){//使用常量引用避免复制构造函数的调用
...
return *this;//返回当前实例的引用
}
指针和引用的区别
- 指针自己占用空间,引用只是一个别名。
- 使用sizeof看一个指针为地址大小,使用sizeof看一个引用为对象大小
- 指针可以被初始化为NULL,引用必须初始化且必须是一个已有对象的引用
- 有const指针,但是无const引用
- 指针在使用中可以指向其他对象,引用一旦绑定不可更改
- 指针可以多级,引用只有一集
- 指针和引用使用运算符++意义不同
- 函数返回动态内存分配的变量,需返回指针。若返回引用不能delete,可能引起内存泄漏。
8.标准输入输出
#include <iostream> //定义了cin、cout、cerr
//使用cerr即时(非缓冲)输出错误信息
cerr << "Error message : " << str << endl;
9.类与结构体在C++中的区别
(1)class 中默认的成员访问权限是 private 的,而 struct 中则是 public 的。
(2)从 class 继承默认是 private 继承,而从 struct 继承默认是 public 继承。
(3)class 可以定义模板,而 struct 不可以。
10.stl库
1)C++ STL 之 vector 的 capacity 和 size 属性区别
size 是当前 vector 容器真实占用的大小,也就是容器当前拥有多少个容器。
capacity 是指在发生 realloc 前能允许的最大元素数,即预分配的内存空间。(拥有该属性的仅有vector和string。list、map等为散列分部,不会有realloc()的调用。)
两个属性分别对应两个方法:resize() 和 reserve()。
使用 resize() 容器内的对象内存空间是真正存在的。
使用 reserve() 仅仅只是修改了 capacity 的值,容器内的对象并没有真实的内存空间(空间是"野"的)。此时切记使用 [] 操作符访问容器内的对象,很可能出现数组越界的问题。
resize()分配了空间并填充了指定大小的初始化对象,reserve()只是预留出了对应的空间,容器内并没有对象,使用[]访问越界。
2)容器与算法
(1)map与set
map和set都是C++关联容器(有顺序的),底层实现使用红黑树:有序(红黑树(平衡二叉搜索树)自动排序) 且键值不重复。(插入式判断是否已经存在)
map与set区别:
- map元素为键值对。set是关键字的简单几何。
- map允许修改value,不允许修改key。set不允许修改元素的值。
- map支持下标操作,可以使用key做下标。set不支持下标。
为什么使用红黑树:
红黑树是一种弱平衡二叉树(不是高度平衡),每个节点添加颜色属性,非黑即红。二叉树本身方便了搜索,若平衡使得对于插入、删除操作较多的情况也比传统的平衡二叉树高,是一种折中的方案(搜索比平衡二叉树慢,插入删除效率比平衡二叉树高(平衡二叉树插入删除要重新调整结构保证平衡))。
- 保证了有序性
- 保证了查找、删除时间复杂度稳定。
(2)map与unordered_map
元素都为 pair,包括键、值。
map按照键进行自动排序,不允许键值重复。
unordered_map底层实现为哈希表,不排序。
(3)vector和list
- vector
连续存储的容器,动态数组,在堆上分配空间。底层实现:数组顺序存储。
访问O(1)支持随机访问。插入:空间不够时,重新申请原有空间2倍,将原空间复制到新空间,再向新空间插入元素。析构释放原空间。 - list
动态链表,在堆上分配空间。底层实现:双向链表 链式存储。
性能:访问:只能快速访问头尾,不支持随机访问。插入删除O(1)
(4)迭代器
迭代器不是指针,是类模板,表现得像指针。把不同容器元素访问逻辑抽象出来,而不暴露内部结构。提供了比指针更高级的行为,根据不同类型实现不同的++,–操作。
(5)使用迭代器删除元素
1.对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;
2.对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
3.对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
11.命名空间
作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。
(1)定义
命名空间可以定义在几个不同的部分中,因此命名空间是由几个单独定义的部分组成的。一个命名空间的各个组成部分可以分散在多个文件中。
所以,如果命名空间中的某个组成部分需要请求定义在另一个文件中的名称,则仍然需要声明该名称。下面的命名空间定义可以是定义一个新的命名空间,也可以是为已有的命名空间增加新的元素:
namespace namespace_name{
//代码
}
(2)调用
namespace_name::code;
::code;//表示全局变量,用于与局部同名变量区分
(3)using指令
可以使用 using namespace 指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。
如果指定了多个命名空间,但之中也有同名元素,会编译报错,编译器仍不知道选择使用哪个命名空间中的元素。
使用using namespace_name::code;指定命名空间特定项目。
12.sleep函数
Linux 用 #include <unistd.h> 和 sleep(),Windos 用 #include <windows.h> 和 Sleep()
Sleep 括号里的时间,在 Windows 下是以毫秒为单位,而 Linux 是以秒为单位。
13.预处理
(1)#define
#define 预处理指令用于创建符号常量。该符号常量通常称为宏,指令的一般形式是:
#define macro-name replacement-text
当这一行代码出现在一个文件中时,在该文件中后续出现的所有宏都将会在程序编译之前被替换为 replacement-text。
#define MIN(a,b) (a<b ? a : b)//参数宏
宏替换只做替换,不做计算,不做表达式处理。
(2)条件编译
有选择地对部分程序源代码进行编译。
#ifdef NULL
#define NULL 0
#endif
define与const常量
- 编译器处理方式不同。define在预处理阶段展开,const是编译运行阶段使用
- 类型和安全检查不同。define宏没有类型,不做类型检查,仅仅是展开。const常量有具体类型,编译阶段执行类型检查,更安全。
- 存储方式不同。宏定义仅仅是预处理时展开,不分配内存。const常量占内存。
- 可以对const常量进行调试,但不能对宏常量调试
- 效率:define定义的常量在内存中多次拷贝(每次编译宏替换分配内存),const常量只有一个空间占用。
- const使用更灵活,可以管理其作用域。
在整个类中恒定的常量:使用枚举常量enum。枚举常量不占用对象的存储空间,但是枚举常量要求为整数。
14.include<>与“”
编译器预处理阶段查找头文件的路径不一样。
对于使用双引号包含的头文件,查找头文件路径的顺序为:
当前头文件目录
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
对于使用尖括号包含的头文件,查找头文件的路径顺序为:
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
15.堆栈静态存储区
堆区: 调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
栈: 使用栈空间存储函数的返回地址、参数、局部变量、返回值
数据段: 存储程序中已初始化的全局变量和静态变量
对于局部对象,常量存放在栈区,对于全局对象,常量存放在全局/静态存储区。对于字面值常量,常量存放在常量存储区。
16.左值右值
定义:
左值是可对表达式取地址、或具名对象、变量。表达式结束后依然存在的持久变量。
右值不能对表达式取地址,或匿名对象。表达式结束就不再存在的临时对象。
左值引用和右值引用
左值引用:左值可以寻址,右值不可
左值可以被赋值,右值不可,但可用来给左值赋值
左值可变,右值一般不可变。
右值引用:
是C++11引入的新特性。使用&&引用一个临时对象。
转移语义
精确传递
18.C++11新特性
auto关键字:自动推导类型。不能用于函数传参和数组类型推导。
nullptr关键字:可被转换成任意指针类型;NULL一般宏定义为0,重载时可能会出问题。
lambda表达式
右值引用
可变参数模板
19.extern“C”
为了实现C++代码调用C代码。指示编译器这部分按照C语言方式编译。C语言不支持重载,C编译时指挥包括函数名。而C++支持重载,编译时既有函数名又有参数类型。使用该语句保证代码以C编译形式编译。
20.定义与声明
定义分配存储空间,还可指定初值。
声明仅表明变量的类型和名字。
定义也是声明。
函数仅写个名字是声明。
extern 变量仅是声明。
21.一个C++源文件从文本到可执行文件的 历程
对于C++源文件,从文本到可执行文件一般需要四个过程:
预处理阶段: 对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
编译阶段: 将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
汇编阶段: 将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
链接阶段: 将多个目标文件及所需要的库连接成最终的可执行目标文件