预处理:预处理相当于根据预处理命令组装成新的C程序(头文件展开,宏定义替换等),不过常以.i为扩展名。
编译: 将得到的.i文件翻译成汇编代码.s文件。
汇编:将汇编文件翻译成机器指令,并打包成可重定位目标程序的.o文件,该文件是二进制文件,字节编码是机器指令。
链接:将引用的其他.o文件并入到我们程序所在的.o文件中,处理得到最终的可执行文件。
宏定义本身可以理解为字符的替换,在实际编程中有很多的应用,宏可以带参数,如:
// An highlighted block
#define SET(a,b) a|(0x1<
#define CLR(a,b) a&(~(0x1<
#define MIN(A,B) ((A)>(B))?B:A
//宏定义参数需要用括号括起来,且表达式内部不能有空格
#define max(a,b) (((a)-(b))&(1<<31))?(b):(a)
#define max(a,b) (a-b)==abs(a-b)?a:b
#define max(a,b) ((((long)((a)-(b)))&0x80000000)?(b):(a))
#define swap(a,b) {a=a+b;b=a-b;a=a-b;}
常量定义必须初始化,对于局部对象,常量存放在栈区,对于全局对象,常量存放在全局/静态存储区。对于字面值常量,常量存放在常量存储区。
1. const的含义
const int a; //整型常量
int const a; //整型常量
const int *a; //指向整型常数的指针(指向的常数不可修改,指针本身可以修改)
int* const a; //指向整数的常指针(指针本身不可修改,指向的整数可以修改)
int const* a const; //指向常量的常指针(都不可修改)
2. 为什么使用const?
(1) 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的)。
(2) 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
(3) 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。
3.const修饰成员函数的目的
const修饰成员函数表明该函数调用不会对对象作出任何更改,事实上,如果确认不会对对象作出修改就应该加上const限定,这样无论是const对象还是普通对象都可以调用该函数。
int num[10] = { 0,1,2,3,4,5,6,7,8,9 };
int len1 = sizeof(num); //len1 = 40
int len2 = sizeof(num) / sizeof(num[0]); //len2 = 10
char str[20] = "0123456789";//存放在堆栈区,可以修改
char *p_str = "0123456789"; //存放在常量区,不可修改
char *p = str;
int len3 = strlen(str); //len3 = 10;
int len4 = sizeof(str); //len4 = 20;
int len5 = sizeof(*str);//len5 = 1
int len6 = sizeof(p); //len6 = 4(32位编译器)
fun(unsiged char *p1, int len)
{
unsigned char* buf = new unsigned char[len+1]
memcpy(buf, p1, len);
}
只能在本文件中使用;局部静态变量保存在数据段,而普通变量分配在栈上,会因为函数栈帧的释放而被释放掉;类中成员加了static,该变量没有this指针,必须通过类名来访问
全局静态变量
内存:存储在静态存储区,在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
局部静态变量
内存:静态存储区
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
作用域:作用域仍为局部作用域,当他的函数或语句结束时,作用域结束,但他没有销毁,仍然驻留在内存中,只不过无法访问,直到再次运行其函数时被调用,而且值不变。
静态函数
在函数的返回类型前加static修饰,那么这个函数只可在本cpp内使用,不会和其他cpp中的同名函数起冲突,也不能被其他文件所使用。
类的静态成员
在类中,静态成员可以实现多个对象间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存在一处,供所有对象公用。
类的静态函数
和类的静态成员一样,都是所有对象共有的。因此,对静态函数的引用不需要用对象名。
静态成员函数实现时不能直接引用类中的非静态成员,但可以直接引用类中的静态成员。如果静态成员函数中要引用非静态成员可以通过对象来引用,引用格式如下:
<类名>::<静态成员函数名> (<参数表>)
设计思想上:
C++面向对象(继承、封装、多态),C面向过程,结构化
语法上:
C++增加许多类型安全的功能,比如强制类型转换。
C++支持范式编程(类模板、函数模板)。
c++调用c函数需要extern C,因为C语言没有函数重载。
extern char *strcpy(char *dest, char *src);
就是把src的字符串复制到dest中,以’\0’结束,例如:
#include
#include
void main(){
char a[20], c[]="where is offer?";
strcpy(a,c);
cout<<a<<endl; //where is offer?
}
void *memcpy(void *dest, const void *src, size_t n);
功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置。例如:
#include
#include
int main() {
char *s = "Golden Global View";//此字符串总共有18个字符,再加上一个结束符,总共存放19个字符
char d1[20], d2[20];
memcpy(d1, s, (strlen(s)+1));//用memcpy函数,把s中字符串的首地址拷贝到d中,连续复制19个字符
memcpy(d2, s+14, 5); //从第14个字符(V)开始复制,连续复制4个字符(View)
printf("%d\n", strlen(s));
printf("%s\n", d1);
printf("%s\n", d2);
return 0;
}
void *memset(void *s, int c, size_t n);//就是把已开辟内存空间s的首n个字节设置为c。
例如
#include
#include
#include
int main()
{
char s[] = "Golden Global View";
memset(s, 'G', 6);//把s中的前6个字符改成G,也就是把Golden改成GGGGGG
printf("%s\n", s);//输出为GGGGGG Global View
return 0;
}
0x3132(0x32是低位,0x31是高位),把它赋值给一个short变量,那么它在内存中的存储可能有如下两种情况:
(1)、大端字节(Big-endian):
----------------->>>>>>>>内存地址增大方向
short变量地址
0x1000 0x1001
0x31 0x32
大端->高位在前->正常的逻辑顺序
(2)、小端字节(little-endian):
----------------->>>>>>>>内存地址增大方向
short变量地址
0x1000 0x1001
0x32 0x31
小端->低位在前->与正常逻辑顺序相反
1. volatile
作用是确保本条指令不会因编译器的优化而省略,且要求==每次直接从内存中读值,==而不是读取寄存器保存的副本
volatile表示这个变量会被意想不到的改变,每次用他的时候都会小心的重新读取一遍,不适用寄存器保存的副本。
(1) 并行设备的硬件寄存器(如:状态寄存器)
(2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
(3) 多线程应用中被几个任务共享的变量
2. register
建议编译器使用寄存器来优化对变量的存取。register修饰符暗示编译程序相应的变量将被频繁地使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存储速度
const_cast, static_cast, dynamic_cast, reinterpret_cast
1. const_cast
用于将const变量转为非const
2. static_cast
用于各种隐式转换,如非const转const,void* 转指针,static_const可用于多态向上转化,如果向下转能成功但是不安全,结果未知
3. dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转换时,如果是非法的对于指针返回NULL,对于引用抛出异常。要深入了解内部转换的原理:它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
4. reinterpret_cast
几乎什么都能转,比如int转指针,但是不安全
5. 不用c强制转换的原因?
容易出错,没有错误检查。
6.隐式类型转换
对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换。
对于只存在单个参数的构造函数来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。
7.RTTI
运行时类型检查,在C++层面主要体现dynamic_cast和typeid,VS中虚函数表的-1位置存放了指向type_info的指针。对于存在虚函数的类型,typeid和dynamic_cast都会查询type_info
操作指针时存在的问题:操作空指针、操作野指针和不经意间改变指针的值,引用可以解决这几个问题,因为引用必须初始化引用某个对象,且不能改为引用其他对象。
智能指针的作用是管理一个指针,因为存在以下情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上避免这个问题,因为智能指针就是一个类,当超出了类的作用域,类会自动调用析构函数释放资源。所以智能指针的作用是在函数结束时自动释放内存空间,不需要手动释放。
1. auto_ptr(C++11已抛弃)
采用所有权模式。
auto_ptr p1(new string("I reigned lonely as a cloud"));
auto_ptr p2;
p2 = p1; //auto_ptr不会报错
p2剥夺了p1的所有权,当程序运行到p1时会报错。所以auto_ptr的缺点是存在潜在的内存崩溃问题。
2. unique_ptr(替换auto_ptr)
unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄漏(例如“new创建对象后因为发生异常而忘记调用delete”)特别有用。
unique_ptr p3(new string("auto"));
unique_ptr p4;
p4 = p3; //此时会报错!
编译器认为p4 = p3 非法,避免了p3不再指向有效数据的问题。因此,unique_ptr更安全。
另外unique_ptr还有更聪明的地方:当程序试图将一个unique_ptr赋给另一个时,如果源unique_ptr是个临时右值,编译器允许这么做;如果源unique_ptr将存在一段时间,编译器将禁止这么做,比如:
unique_ptr pu1(new string ("hello world"));
unique_ptr pu2;
pu2 = pu1; // #1 not allowed
unique_ptr pu3;
pu3 = unique_ptr(new string ("You")); // #2 allowed
注:如果想执行#1类似的操作,要安全的重用这种指针,可给它赋新值,使用std::move(),例如:
unique_ptr ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
3. shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时释放。从名字share可以看出该资源被多个指针共享,它使用计数机制来表明资源被几个指针共享。除了可以通过new来构造,还可以通过传入auto_ptr,unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源被释放。
shared_ptr是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。
其成员函数包括:
use_count返回引用计数的个数
unique返回是否独占所有权(use_count为1)
swap交换两个shared_ptr对象(即交换所拥有的对象)
reset放弃内部对象的所有权或拥有对象的变更,会引起原有对象引用计数的减少
get返回内部对象(指针),由于已经重载了()方法,因此和直接使用对象是一样的。如
shared_ptr sp(new int(1));
sp和sp.get()是等价的
4. weak_ptr
weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象,进行该对象的内存管理,weak_ptr只是该对象的一个访问手段。weak_ptr设计的目的是为配合shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用计数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题。如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。例如:
class B;
class A {
public:
shared_ptr pb_;
~A() {
cout<<"A delete\n";
}
};
class B {
public:
shared_ptr pa_;
~B() {
cout<<"B delete\n";
}
};
void fun() {
shared_ptr pb(new B());
shared_ptr pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<
可以看到fun函数中pa,pb互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者的引用计数还为1,导致跳出函数时资源没有被释放(A,B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的
shared_ptr pb_;
改为
weak_ptr pb_;
这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。
注意我们不能通过weak_ptr直接访问对象的方法,比如对象中有一个方法print(),我们不能这样访问
pa->pb_->print();
pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:
shared_ptr p = pa->pb_.lock();
p->print();
程序中已动态分配的堆内存由于某种原因没有得到释放或者无法释放,造成系统资源的浪费,导致程序运行速度减慢甚至崩溃。
避免的方法:
首先程序员要养成良好习惯,保证malloc/new和free/delete匹配;
检测内存泄漏的关键原理就是,检查malloc/new和free/delete是否匹配,一些工具也就是这个原理。要做到这点,就是利用宏或者钩子,在用户程序与运行库之间加了一层,用于记录内存分配情况。
windows工具:VC自带的CRT:_CrtCheckMemory 调试器和 CRT 调试堆函数,Visual Leak Detector,bounds checker
linux调试工具:MEMWATC Valgrind memcheck
将可能被继承的父类的析构函数设置为虚函数,可以保证我们new一个子类对象,然后使用基类指针指向该子类对象时,释放基类指针可以释放子类的空间,防止内存泄漏。
c++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,会占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此默认的析构函数不是虚函数。而只有需要当作父类时,设置为虚函数。
析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,例如~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。
如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。
如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数,在销毁类之前,释放掉申请的内存空间,避免内存泄漏。
类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。
1.定义:指向函数的指针
C编译时,每个函数都有一个入口地址,该入口地址就是函数指针指向的地址。可以用该指针调用函数
2.用途:调用函数和做函数的参数,比如回调函数
3.实例:
char *fun(char *p){} //函数指针fun
char *(*pf)(char *p){};//函数指针pf
pf = fun; //函数指针pf指向函数fun
pf(p); //通过函数指针调用函数fun
静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加以此内存开销。
重载:函数名相同,参数列表不同(个数,类型),返回值没有要求,需要在同一作用域中。
重写:函数名相同,参数列表相同,子类继承父类时重新定义父类中的虚函数
静态:函数重载,编译时已经确定。
动态:虚函数机制,运行期间动态绑定。例如:一个父类指针指向子类对象,使用父类的指针调用子类重写过的父类中的虚函数的时候,会调用子类重写后的函数。
虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。
new/delete是c++的关键字,自动分配内存空间,调用构造函数和析构函数
malloc和free是c语言的库函数,使用必须指明申请内存空间的大小
构造函数是一种特殊的类成员函数,是当创建一个类的对象时,它被调用来对类的数据成员进行初始化和分配内存。(构造函数的命名必须和类名完全相同)
构造函数可以显式调用,也可以自动调用。
拷贝构造函数是C++独有的,它是一种特殊的构造函数,用基于同一类的一个对象构造和初始化另一个对象。
当没有重载拷贝构造函数时,通过默认拷贝构造函数来创建一个对象
A a;
A b(a); //都是拷贝构造函数来创建对象b
A b=a; //都是拷贝构造函数来创建对象b
强调:这里b对象是不存在的,是用a 对象来构造和初始化b的!!
在C++中,3种对象需要复制,此时拷贝构造函数会被调用
1)一个对象以值传递的方式传入函数体
2)一个对象以值传递的方式从函数返回
3)一个对象需要通过另一个对象进行初始化
什么时候编译器会生成默认的拷贝构造函数?
1)如果用户没有自定义拷贝构造函数,并且在代码中使用到了拷贝构造函数,编译器就会生成默认的拷贝构造函数。但如果用户定义了拷贝构造函数,编译器就不在生成。
2)如果用户定义了一个构造函数,但不是拷贝构造函数,而此时代码中又用到了拷贝构造函数,那编译器也会生成默认的拷贝构造函数。
因为系统提供的默认拷贝构造函数工作方式是内存拷贝,也就是浅拷贝。如果对象中用到了需要手动释放的对象,则会出现问题,这时就要手动重载拷贝构造函数,实现深拷贝。
下面说说深拷贝与浅拷贝:
浅拷贝:如果复制的对象中引用了一个外部内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象指向同一个外部内容,就是浅拷贝。(指针虽然复制了,但所指向的空间内容并没有复制,而是由两个对象共用,两个对象不独立,删除空间存在)
深拷贝:如果在复制这个对象的时候为新对象制作了外部对象的独立复制,就是深拷贝。需要手动编写拷贝构造函数实现深拷贝
拷贝构造函数重载声明如下:
**A (const A& other)**
当一个类的对象向该类的另一个对象赋值时,就会用到该类的赋值函数。
当没有重载赋值函数(赋值运算符)时,通过默认赋值函数来进行赋值操作
A a;
A b;
b=a;
强调:这里a,b对象是已经存在的,是用a 对象来赋值给b的!!
赋值运算的重载声明如下:
A& operator = (const A& other)
参考原文
1.2.3.