static关键字的作用:
(1)函数体内static变量的作用范围为该函数体,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
(2)在模块内的static全局变量和函数可以被模块内的函数访问,但不能被模块外其它函数访问;
(3)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
(4)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
const关键字的作用:
(1)阻止一个变量被改变
(2)声明常量指针和指针常量
(3)const修饰形参,表明该输入参数在函数内部不能改变其值
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量(const成员一般在成员初始化列表处初始化)
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为”左值”。
extern关键字的作用:
(1)extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
(2)extern "C"的作用是让 C++ 编译器将extern "C"声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接。
(1)sizeof是运算符,而strlen是函数;
(2)sizeof的用法是sizeof(参数),这个参数可以是数组,指针,类型,对象,甚至是函数,其值在编译的时候就计算好了,而strlen的参数必须是字符型指针(char*),其值必须在函数运行的时候才能计算出来;
(3) sizeof的功能是获得保证能容纳实现的建立的最大对象的字节的大小,而strlen的功能是返回字符串的长度,切记这里的字符串的长度是包括结束符的;
(4)当数组作为参数传递给函数的时候,传的是指针,而不是数组,传递数组的首地址;
char str[20] = "0123456789";
int a = strlen(str); //10 运行时计算
int b = sizeof(str);//20 编译时确定
(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。
(2)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
(3)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化
(4)指针的值在初始化后可以改变,即指向其它的存储单元,而引用初始化后就不会再改变。
(5)"sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小。
(6)作为参数传递时,二者有本质不同:指针传参本质是值传递,被调函数的形参作为局部变量在栈中开辟内存以存放由主调函数放进来的实参值,从而形成实参的一个副本。而引用传递时,被调函数对形参的任何操作都会通过一个间接寻址的方式影响主调函数中的实参变量。
如果想通过指针参数传递来改变主调函数中的相关变量,可以使用指针的指针或者指针引用。
(1)指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身的大小决定,每一个元素都是一个指针,在32 位系统下任何类型的指针永远是占4 个字节。它是“储存指针的数组”的简称。
(2)数组指针:首先它是一个指针,它指向一个数组。在32 位系统下任何类型的指针永远是占4 个字节,至于它指向的数组占多少字节,不知道,具体要看数组大小。它是“指向数组的指针”的简称。
int arr[] ={1,2,3,4,5};
int *ptr =(int *)(&arr+1); //指向arr末尾后的地址,即指向紧接arr[4]的后一块int array[5]类型的内存空间
int *ptr2 =(int *)(arr+1); //指向数组arr第二个元素的地址 *ptr2 == &(arr[1])
cout<<*(ptr-1)<<" "<<*(ptr2-1)<
数组指针(行指针)
int a[2][3] = {{1,2,3},{4,5,6}};
int (*p)[3]; //定义数组指针
p = a;
p++;
cout<<**p<
(3)函数指针(待补充)
(1)栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量值等,其操作方法类似数据结构中的栈。
(2)堆区(heap):一般由程序员分配释放,与数据结构中的堆毫无关系,分配方式类似于链表。
(3)全局/静态区:全局变量和静态变量的存储是放在一起的,在程序编译时分配。
(4)文字常量区:存放常量字符串。
(5)程序代码区:存放函数体(类的成员函数、全局函数)的二进制代码
int a=0; //全局初始化区
char *p1; //全局未初始化区
void main()
{
int b; //栈
char s[]="bb"; //栈
char *p2; //栈
char *p3="123"; //其中,“123\0”常量区,p3在栈区
static int c=0; //全局区
p1=(char*)malloc(10); //10个字节区域在堆区
strcpy(p1,"123"); //"123\0"在常量区,编译器 可能 会优化为和p3的指向同一块区域
}
C/C++内存分配有三种方式:
(1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
(2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。
栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。
动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏。
另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
补充:从编译的角度出发的内存划分:
可执行文件的结构(还未放入内存的程序)
1)BSS区:(Block Started by Symbol,以符号开始的块) 存放未初始化的变量
2)数据区:存放已经初始化的全局变量,静态变量和常量数据
3)代码区:存放可执行代码
进程内存结构:
1)栈区:存放函数参数,返回值和局部变量。该区由编译器自动分配和释放,由操作系统自动管理(向下生长)。
2)堆区:堆区存放由malloc函数,new操作符等用户自主分配的变量和对象,并使用free/delete来释放内存空间。
3)BSS区:存放未初始化的全局变量,默认为0
4)数据区:存放静态变量,已经初始化的全局变量以及常量
5)代码区:存放可执行代码
(1)申请方式
stack:
由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = new char[10] ;
但是注意p1、p2本身是在栈中的
2)申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆: 首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲 结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
(3)申请大小的限制及生长方向
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也可能是1M,它是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小 。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
(4)申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,它既不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。
(5)堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
(1)malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
(2)对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
(3)C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
(4)new可以认为是malloc+构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void指针
(1)内存尚未分配成功,却使用了它;
解决办法:在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口使用assert(p != NULL) 进行检查,如果是用malloc或者new来申请的,应该用
**if (p == NULL)**或者 **if (p != NULL)**来进行防错处理。
(2)内存分配虽然成功,但在使用前未初始化;
错误原因:一是没有初始化的观念,二是误以为内存的缺省初值全为零,导致引用初值错误(如数组)。
解决办法:内存的缺省初值是什么并没有统一的标准,尽管有些时候为零值,但是宁可信其有,不可信其无,无论以何种方式创建数组,都要赋初值。
(3)内存分配成功并初始化,但是超过了内存的边界;
这种问题常出现在数组越界,写程序是要仔细。
(4)忘记释放内存,造成内存泄露;
含有这种错误的函数每次被调用都会丢失一块内存,开始时内存充足,看不到错误,但终有一次程序死掉,报告内存耗尽。
(5)释放了内存却继续使用它
产生原因:
1.程序中的对象调用关系过于复杂,难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
2.函数return语句写错了,注意不要返回指向“栈内存”的指针或者引用,因为该内存在函数体结束时理论上被自动销毁。
3.使用free或者delete释放了内存后,没有将指针设置为null,导致产生野指针。
内存管理需要遵循的规则
(1)用malloc 或者 new 申请内存之后,应该立即检查指针值是否为 NULL ,防止使用指针值为NULL的内存;
(2)不要忘记数组和动态内存赋初值,防止未被初始化的内存作为右值使用;
(3)避免数组或者指针下标越界,特别要当心“多1”或者“少1”的操作;
(4)动态内存的申请与释放必须配对,防止内存泄露;
(5)用free或者delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”;
为什么要使用字节对齐?
字节对齐是C/C++编译器的一种技术手段,主要是在可接受空间浪费的前提下,尽可能地提高对相同元素过程的快速处理。(比如32位系统,4字节对齐能使CPU访问速度提高)
需要字节对齐的根本原因在于CPU访问数据的效率问题。
字节对齐的原则
(1)结构体中每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会填充字节
(2)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会填充字节。
【注意】:对于结构体中间嵌套结构体的情况,嵌入的结构体也需要满足以上要求。
当然这里还要考虑#pragma pack(n)伪指令的影响,如果有取较小值。
// 用于测试的结构体,针对64位系统
typedef struct
{
int i; // 4 bytes
char c; // 1 bytes
char d; // 1+2 bytes
long j; // 8 bytes
} A; //占用16字节
typedef struct
{
short s[3]; // 2 * 3 = 6 bytes
char c[3]; //3 * 1 + 1 = 4 bytes
}B;//共占用10字节
typedef struct
{
B b[2]; //10 * 2 + 4 = 24 bytes
A a; //16 bytes
}C; //共占40字节
1. int型变量
if ( n == 0 )
if ( n != 0 )
2. bool型
if (value == 0)
if (value != 0)
3. char*型
if(p == NULL) / if(p != NULL)
5. 浮点型
const float EPSINON = 0.0000001;
if ((x >= - EPSINON) && (x <= EPSINON)
优点:函数会在它所调用的位置上展开。这么做可以消除函数调用和返回所带来的开销(寄存器存储和恢复),而且,由于编译器会把调用函数的代码和函数本身放在一起优化,所以也有进一步优化代码的可能。
内联函数使用的场合:对于简短的函数并且调用次数比较多的情况,适合使用内联函数。
内联函数和宏定义区别:
1)内联函数在编译时展开,而宏在预编译时展开
2)在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
3)内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能。
4)宏不是函数,而inline是函数
添加链接描述
调用惯例: 函数在被调用时,函数的调用方和被调用方对于函数时如何调用的必须有一个明确的规定。只有双方同时遵循同样的规定,函数才能够被正确调用。这样的规定被称为:调用惯例。
函数的调用惯例包含两个方面:
1.函数参数的传递顺序和方式
函数的传递有很多种方式,最常见的是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序,是从左往右压栈,还是从右往左压栈
2.栈的维护方式
在函数将参数压入栈中之后,函数体会被调用,此后需要将被压入的参数全部弹出,使得栈在函数调用前后保持一致。这个弹出的工作可以由函数调用方来完成,也可以由函数本身来完成。在不指定调用惯例的情况下,默认采用cdecl惯例。
调用惯例 | 出栈方 | 参数传递 |
---|---|---|
cdecl | 函数调用方 | 从右至左参数入栈 |
stdcall | 函数本身 | 从右至左参数入栈 |
fastcall | 函数本身 | 前面两个参数由寄存器传递,后面的参数通过堆栈传递 |
pascal | 函数本身 | 从左至右入栈 |
(1)重载:重载翻译自overload,是指同一可访问区内被声明的几个具有不同参数列表(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
(2)重写:重写翻译自override,是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致,只有函数体不同。
1.成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
2.覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
3.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
static_cast( expression )
dynamic_cast < type-id> ( expression )
4. 只能用于对象的指针和引用之间的转换,需要虚函数。
5. dynamic_cast会检查转换是否会返回一个被请求的有效的完整对象,否则返回NULL;
6. Type-id必须是类的指针、类的引用或者void *,用于将基类的指针或引用安全地转换成派生类的指针或引用。
const_cast < type-id> ( expression )
这个转换类型操纵传递对象的const属性,或者是设置或者是移除。
reinterpret_cast < type-id> ( expression )
用在任意指针类型之间的转换;以及指针与足够大的整数类型之间的转换,从整数到指针,无视大小。
隐式类型转换
1. 相同点:
都是地址的概念
指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名
2. 不同点
指针是一个实体,而引用仅仅是一个别名
引用使用时无需解引用(*),指针需要解引用;
引用只能在定义时被初始化一次,之后不可变(从一而终);指针可变;
引用没有 const,指针有 const,const 的指针不可变;
引用不能为空,指针可以为空;
“sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
typeid(T) == typeid(T&) 恒为真,sizeof(T) == sizeof(T&) 恒为真,但是当引用作为成员时,其占用空间与指针相同(没找到标准的规定)。
指针和引用的自增(++)运算意义不一样;
指针和引用更加详细的解释链接
对于C/C++编写的程序,从源代码到可执行文件,一般经过下面四个步骤:
进程是资源(内存,CPU等)分配的基本单位,是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源。每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段(这种操作非常昂贵)。
线程是程序执行的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。
/* 简单的多线程实例 */
#include
#include //用于定义thread对象
#include //Sleep()函数
using namespace std;
void increase(int *num, int count) //每隔100ms进行num++
{
for (int i = 0; i < count; i++) {
(*num)++;
Sleep(100);
}
}
void disp(int *num,int count) //每隔1s输出一次num
{
for (int i = 0; i < count; i++) {
cout << "num = " << dec << *num << endl;
Sleep(1000);
}
}
int main()
{
int num = 0;
thread thread1(increase, &num, 1000); //创建进程对象,并将函数及其参数传入
thread thread2(disp, &num,1000); //同上
thread1.join(); //加入线程
thread2.join(); //加入线程
system("pause");
return 0;
}
结论:构造函数不可以是虚函数,析构函数可以,而且经常为虚函数。
构造函数为什么不可以为虚函数?
a) 从面向对象的角度来考虑,子类在声明对象时,会调用父类的构造函数,如果这时候子类中的函数名和父类名相同,则会因为函数覆盖出现无法正常构造父类成员的错误。
b) 从对象的实现来看,虚函数实现多态是通过维护一个虚函数表vTable来实现,而虚函数表要在构造函数调用后才生成,如果将构造函数声明为虚函数,则无法生成对象。另外,因为构造函数的主要作用是为对象开辟内存空间,并进行相关的初始化操作,每个对象在生成时只会执行一次,不涉及到多态性,因此也没有必要设置成虚函数
析构函数为虚函数
在利用父类指针p指向子类对象,从而实现多态的过程中,如果不将父类的析构函数设置为虚函数,则当进行delete p操作时候,会调用父类的析构函数,因此会导致内存释放不完全的问题。通过将父类的析构函数设置为虚函数,则delete p操作会正确调用子类的析构函数。
example1:
#include
using namespace std;
class base
{
public:
base() {}
~base() {
cout<<"enter deconstructor of base"<
example2:
#include
using namespace std;
class base
{
public:
base() {}
virtual ~base() {
cout<<"enter deconstructor of base"<
#include
using namespace std;
int main()
{
int a = 1;
int &ra = a; //对变量a的引用--浅拷贝
int *pa = &a; //对变量a去地址--浅拷贝
int *pb = new int(a); //构造一个新的变量,并用a对其进行初始化
cout<<"ra = "<
结论:应该用long类型
int 类型的长度为4个字节,而long类型的不定,但是和操作系统的位数保持一致(如32位操作系统的long型长度为32bit,4个字节,64位操作系统的long型长度为64bit,8个字节)。如果用int类型传递64位操作系统的指针,会把高4位地址截断,导致错误。反观long型,由于其字节长度和操作系统位数保持一致,因此不会产生地址截断的问题。
1)关键字及新语法:auto、nullptr、for
2)STL容器:std::array、std::forward_list、std::unordered_map、std::unordered_set
3)多线程:std::thread、std::atomic、std::condition_variable
4)智能指针内存管理:std::shared_ptr、std::weak_ptr
5)其他:std::function、std::bind和lamda表达式