c++基础

C++基础
1.static关键字的作用
       ①全局静态变量:存放于静态存储区,在程序整个运行期间都不释放。只能作用于本文件,不能被其他文件引用。
       ②局部静态变量:存放于静态存储区,在程序整个运行期间都不释放。作用域仍为局部作用域(即定义它的函数或者语句块)。当离开作用域后,并没有销毁,直到再次被调用。
       ③静态数据成员:可以实现多个对象之间的数据共享,并且静态成员是类的所有对象中共享的成员,而不是某个对象的成员,只能在类外进行初始化。可以通过类名或对象名调用。并且只要在类中指定静态数据成员,即使不定义对象,也会为其分配空间。
       ④静态函数(内部函数):只能在本文件中调用后,不能被其他文件调用。
       ⑤静态成员函数:静态成员函数是属于类,而非某个对象,可以通过类名或对象名调用。一般只能访问静态数据成员,若访问非静态数据成员须加上对象名。

2.用extern声明外部变量和外部函数
       当多个源程序文件编译成一个程序后,可在一个文件中声明外部变量或函数来使用其他文件的外部变量(全局变量)或函数。
在这里插入图片描述
3.引用和指针的区别
       ①指针存储一个变量的地址,指向一块地址空间,而引用只是一个别名。
       ②使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小。
       ③引用在创建时必须初始化,指针可以在任何时候初始化.引用初始化后不允重新成为其他对象的引用,指针则可以随意指向其他对象。指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用。
        ④指针和引用使用++运算符的意义不一样。
        ⑤指针可以有多级指针(**p),而引用只有一级。
                                                      在这里插入图片描述

4.函数指针
        函数指针是指向函数的指针变量。C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址,可用该指针变量调用函数。
                                                在这里插入图片描述

5.malloc/free与new/delete区别
        ①malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
        ②malloc/free不能执行类的构造函数与析构函数,只是单纯申请一块内存。而new/delete创建对象时会构造函数。如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。
在这里插入图片描述
        ③malloc申请之后返回的是void*,而new返回的指针有类型。
        ④malloc分配的内存可以用realloc扩容。new无法扩容。
        char *str;         str = (char *) malloc(15);         str = (char *) realloc(str, 25);
       在这里插入图片描述

6.野指针
       “野指针”不是NULL指针,是指向“垃圾”内存的指针(野指针就是指向一个已删除的对象或者未申请访问受限内存区域的指针)。“野指针”的成因主要有两种:
       ①指针变量没有被初始化。(指针不会自动赋为NULL,须自己设为NULL).
       ②指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。
       ③指针操作超越了变量的作用域范围。
                            在这里插入图片描述
7.一个C/C++编译的程序占用内存分为以下几个部分
        (程序占用的存储空间分为程序区,动态存储区(堆、栈等),静态存储区)
       ①栈:由编译器自动分配和释放,用于存放局部变量和参数
        ②堆:手动分配和释放的,如果没有手动释放,程序结束后由系统回收,用malloc/free,new/delete来申请和释放。
        ③全局/静态存储区:存放全局变量和静态变量,在程序编译时分配。
        ④常量存储区:存放不允许修改的常量。
        ⑤代码区:存放程序编译后的二进制代码。
int a = 0; //全局初始化区
char *p1; //全局未初始化区
void main()
{
int b; //栈
char s[] = “abc”; //栈
char *p2; //栈
char *p3 = “123456”; //123456{post.content}在常量区,p3在栈上
static int c = 0; //全局(静态)初始化区
p1 = (char *)malloc(10); //分配得来得10字节的区域在堆区
p2 = (char *)malloc(20); //分配得来得20字节的区域在堆区
strcpy(p1, “123456”);
//123456{post.content}放在常量区,编译器可能会将它与p3所指向的"123456"优化成一块
}
在这里插入图片描述
在这里插入图片描述

8.内存分配方式有三种:
  ①从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整
个运行期间都存在。例如全局变量,static变量。
  ②在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数
执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,
但是分配的内存容量有限。
  ③从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少
的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决
定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内
存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
9.栈和堆的区别
       ①管理方式不同:栈是由编译器自动管理,堆释放工作由程序员控制,容易产生内存泄漏。
  ②空间大小不同:一般在32位系统下,堆内存可以达到4G的空间。堆的大小受限于计算机系统中有效的虚拟内存(硬盘)。在VC6下面默认的栈空间大小是1M。
  ③能否产生碎片不同:频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题。因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。
  ④生长方向不同:堆的生长方向是向上的,也就是向着内存地址增加的方向;栈的生长方向是向下的,是向着内存地址减小的方向增长。即栈顶的地址和栈的最大容量是系统预先规定好的。如果申请的空间超过栈的剩余空间,会提示overflow。操作系统有一个记录内存空闲地址的链表,当系统收到申请时,会遍历该链表,寻找一个空间大于申请空间的堆结点,并将该堆结点从链表中删除。
       ⑤分配方式不同:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。(调用 alloca的函数返回的时候, 它分配的内存会自动释放)
       ⑥分配效率不同:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
       从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
10.内存的静态分配和动态分配的区别
        一是时间不同。静态分配发生在程序编译和连接的时候。动态分配则发生在程序调入和执行的时候。
       二是空间不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。堆的动态分配由函数malloc/new进行分配。不过栈的动态分配(alloca)和堆不同,他的动态分配是由编译器进行释放,无需我们手工实现。 在调用 alloca的函数返回的时候, 它分配的内存会自动释放。
int *pt = (int *)alloca(sizeof(int)*10);
free(pt);//此时不能用free()去释放,会导致错误

11.内存泄漏
       内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。分配内存后,没有进行释放,导致这块内存就不能被再次使用,造成了内存的浪费。
       内存泄漏的分类:
       ①堆内存泄漏:通过malloc,realloc new等从堆中分配的一块内存,使用完成后没有用free或者delete 释放,导致这块内存将不会被使用,就会产生内存泄漏。
       ②系统资源泄露:由操作系统分配的资源比如 Bitmap(位图),handle (句柄),SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费。
       ③没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
       ④智能指针的循环引用
12.如何检测内存泄漏
       检测内存泄漏的关键是检测内存分配函数和内存释放函数的调用是否匹配。内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete。为了判断内存是否泄露,我们一方面可以使用linux环境下的内存泄漏检查工具Valgrind,mtrace,windows环境下用Purify,BoundsChecker等。 另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。
如何避免内存泄漏
        ①尽量把内存的分配和释放操作封装在类里面,自动化地进行分配和释放。
       ②尽量不去手动分配内存,可以使用STL里的容器
       ③尝试在适用的情况下使用智能指针
       ④使用了内存分配的函数,要记得使用相应函数释放掉内存
13.智能指针
       智能指针是行为类似于指针的类对象,可以将new获得的地址赋给这种对象,当智能指针过期时,会自动调用析构函数释放内存空间。智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。
有四种类型auto_ptr(c++98的方案,c++11已经抛弃)、unique_ptr、shared_ptr、weak_ptr。
       ①auto_ptr:采用所有权模式,对于特定对象,只能有一个智能指针拥有它,赋值操作会转让所有权。
在这里插入图片描述
此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!

       ②unique_ptr(替换auto_ptr):采用所有权模式,对于特定对象,只能有一个智能指针拥有它,赋值操作会转让所有权。
在这里插入图片描述
       编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。
       另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:
在这里插入图片描述
如果确实想执行赋新值操作可以使用move()函数 在这里插入图片描述
       ③shared_ptr:实现共享式拥有概念。多个智能指针可以指向相同对象,它使用计数机制来表明资源被几个指针共享。赋值时计数加1,指针过期时,计数减1,当计数等于0时,资源会被释放。
       shared_ptr 可以通过三种方式得到:
       ⑴通过一个指向堆上申请的空间的指针初始化(切记不要用栈上的指针,否则,当智能指针全部释放控制权(栈中的对象离开作用域本身就会析构一次),将会析构对象,导致出错)
       ⑵通过make_shared函数得到
       ⑶通过另外一个智能指针初始化
       注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
在这里插入图片描述
       成员函数:
       use_count() 返回引用计数的个数
       make_shared() 构造shared_ptr
       unique() 返回是否是独占所有权( use_count 为 1)
       swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
       reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
       get() 返回内部对象(指针)
***当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。可以使用weak_ptr 来解决
在这里插入图片描述
       如上代码,将在程序退出前,father的引用计数为2,son的计数也为2,退出时,shared_ptr所作操作就是简单的将计数减1,如果为0则释放,显然,这个情况下,引用计数不为0,于是造成father和son所指向的内存得不到释放,导致内存泄露。

       ④weak_ptr :是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
在这里插入图片描述
14.命名空间
       命名空间是ANSIC++引入的可以由用户命名的作用域,用来处理程序中常见的同名冲突。
        当两个源程序文件A、B中有同名的全局变量或函数时,在分别对文件A和文件B进行编译时不会有问题。但是,如果一个程序包括文件A和文件B,那么在进行连接时,会报告出错,因为在同一个程序中有两个同名的变量,认为是对变量的重复定义。全军变量的作用域是整个程序,同一个作用域中不应有两个同名的实体。
15.C++多态性
       不同对象对于同一个消息有不同的响应。
       多态性分为静态多态性和动态多态性。
       静态多态性是通过函数重载实现的(运算符重载实质上也是函数重载),在程序编译时就能确定要调用的是哪一个函数。
       动态多态性是通过虚函数与指向基类对象的指针变量配合使用来实现的,在程序运行过程中才能确定调用的是哪一个函数。
16.虚函数
       在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。对于一个父类的对象指针类型变量,如果给他赋父类对象的指针,那么他就调用父类中的函数,如果给他赋子类对象的指针,他就调用子类中的函数。函数执行之前查表
       在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。虚函数表是针对类的,一个类的所有对象的虚函数表都一样。
       C++ 类中有虚函数的时候有一个指向虚函数表的指针(_vfptr),在32位系统分配指针大小为4字节。无论多少个虚函数,只有这一个指针,4字节。一般的函数是没有这个指针的,而且也不占类的内存。
       ①纯虚函数就是定义了一个虚函数但并没有实现,原型后面加"=0"。包含纯虚函数的类都是抽象类,不能生成实例。virtual void display()=0;
17. 构造函数与析构函数
       在建立一个对象时,先调用基类构造函数,再执行派生类构造函数本身。释放对象时,先执行派生类析构函数,再执行基类析构函数。
       在执行派生类析构函数时,系统会自动调用基类析构函数。
       构造函数和析构函数中可以调用虚函数,调用虚函数同调用一般的成员函数一样。
       基类的析构函数应声明为虚函数,这样当delete作用与指向派生类对象的基类指针时,系统会调用派生类的析构函数,否则只调用基类的析构函数。
       将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
       C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

18.虚基类(虚继承)
       在声明派生类时将基类声明为虚基类。class B:virtual public A
       为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承。经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,即基类成员只保留一次。
       注意:在最后的派生类中不光要对其直接基类进行初始化,还要对虚基类初始化。
       C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和C)对虚基类的构造函数的调用,这样可以保证虚基类的数据成员不会被多次初始化。
19.重载、重写(覆盖)、重定义(隐藏)
       重载:函数名相同,但参数列表不同(个数,类型,顺序),在同一作用域中
       重写:在基类中声明为虚函数,在派生类中重新定义该函数的函数体,函数名和参数列表必须相同。
       重定义:在基类中声明一个函数,在派生类中重新定义该函数,函数名相同,参数列表或函数体不同。
20.继承、基类与派生类的转换
       派生:在一个已有类的基础上建立一个新类。
       继承:一个新类从已有的类那里获得其已有的属性。
可以用公用派生类对象对基类对象赋值,反之则不可。
可以将公用派生类对象的地址赋给基类指针,但该指针指向的是派生类中的基类部分,只能访问派生类中的基类成员。
21.C++从源文件到可执行文件的过程
       对于C++源文件,从文本到可执行文件一般需要四个过程:
       预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
       编译阶段:将经过预处理后的预编译文件转换成相应汇编代码,生成汇编文件
       汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
       链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
       ①静态链接:
       在程序运行之前,将多个目标文件及所需要的库连接成一个完整的模块。
       空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;
       更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
       运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
       ②动态链接:
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序(装入时动态链接)。或当程序要运行时,首先将主程序和立刻要用到的目标程序装入到内存中,在运行过程中如果要用到某个目标程序,才将目标程序调入内存并链接。
       共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;
       更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
       性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值