1.C++面向对象的三大特征?
1)封装:将客观事物封装成抽象的类,并且设计者可以对类的成员进行访问控制权限控制.
这样一方面可以做到数据的隐藏,保护数据安全;另一方面,封装可以修改类的内部
实现而不用修改调用了该类的用户的代码.同时封装还有利于代码的方便复用;
2)继承:a.继承具有这样一种功能,它可以使用现有类的所有功能;并且可以在不重新编写原有
类的情况下对类的功能进行扩展.
继承的过程是一般到特殊的过程,即是它们是is-a的关系;
基类或父类是一般,而子类或派生类是基类的特殊表现;
要实现继承可以通过继承和组合来实现;
b.广义上的继承分成三大类:
实现继承:使用基类的属性和方法而无需额外编码的能力;
接口继承:接口继承是指仅使用基类的属性和方法的名称,而具体的实现子类必须自己完成的能力;
可视继承:子窗体(类)使用父窗体(类)的外观和实现代码的能力;
3)多态:a.多态的实现分成两种,一种是编译时的多态,主要是通过函数重载和运算符重载来实现的,是通过静态联编实现的;
另外一种是运行时多态,主要是通过函数覆盖来实现的,它需要满足3个条件:基类函数必须是虚函数,并且基类的指针
或引用指向子类的时候,当子类中对原有的虚函数进行重新定义之后形成一个更加严格的重载版本的时候,就会形成
多态;它是通过动态联编实现的;
b.运行时的多态可以让基类的指针或引用指向不同的对象的时候表现出来不同的特性;
2.简述C/C++程序编译时的内存分配情况
1)一般一个c/c++程序编译的时候内存布局如下(地址从低到高的顺序)
a.代码区:存放程序的二进制代码.
b.常量区:这个区和代码区的距离很近,主要存放一些非局部常量值和字符串字面值,一般不允许修改,程序结束由系统释放;
具有常属性并且初始化的全局和静态变量也放在这个区.
c.数据区:赋过初值的且不具有常属性的静态和全局变量在数据区.它和BSS段统称为静态区;程序结束后由系统释放;
d.BSS段:没有初始化的静态和全局变量;进程一旦被加载这个区所有的数据都被清0;
e.堆区: 动态分配的内存;由程序员分配和释放,程序结束的时候如果没有释放,则由OS回收;
f.栈区: 由编译器自动分配和释放,不使用的时候会自动的释放.主要用来存放非静态的局部变量,函数的参数和返回值,
临时变量等.
g.命令行参数和环境变量区;
下面是对应一段经典的代码:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> //unix下的头文件 const int const_global = 10; //常全局变量 int init_global = 10; //初始化的全局变量 int uninit_global; //未初始化的全局白能量 int main(int argc,char * argv[]) { const static int const_static = 10;//常属性的静态变量,不可以被赋值,初始化 static int init_static = 10; //初始化静态变量 static int uninit_static; //未初始化静态变量 const int const_local = 10; // 常属性的局部变量 int prev_local = 1;//前局部变量 int next_local = 5;//后局部变量 int* prev_heap = malloc(sizeof(int));//前面分配的堆变量 int* next_heap = malloc(sizeof(int));//后面分配的堆变量 const char* literal = "literal"; //字符串字面值,字面值常量 extern char** environ; // 环境变量 printf("----地址最高断命令行参数和环境变量------- "); printf(" 环境变量:%p ",environ); printf(" 命令行参数:%p ",argv); printf("---------------栈区----------------------- "); printf(" 常局部变量:%p ",&const_local); printf(" 前局部变量:%p ",&prev_local); printf(" 后局部变量:%p ",&next_local); printf("--------------------堆-------------------- "); printf(" 前堆变量:%p ",prev_heap); printf(" 后堆变量:%p ",next_heap); printf("--------------------BSS-------------------- "); printf("未初始化全局变量:%p ",&uninit_global); printf("未初始化静态变量:%p ",&uninit_static); printf("----------------数据------------------------ "); printf(" 初始化全局变量:%p ",&init_static); printf(" 初始化全局变量:%p ",&init_global); printf("----------------代码区---------------------- "); printf(" 常静态变量:%p ",&const_static); printf(" 字面值常量:%p ",&literal); printf(" 常全局变量:%p ",&const_global); printf(" 函数:%p ",main); return 0; }
2)从上面可以看出c/c++的内存分配方式主要有三种?
a.从静态存储区域分配:
内存在程序编译时已经分配好,这块内存在程序的整个运行期间都存在.
速度快,不容易出错.因为由系统会善后.
b.在栈上分配内存:
在执行函数的时候,函数内非静态局部变量的存储单元都是在栈上创建,函数执行
结束的时候这些存储单元自动被释放.栈内存分配内置于处理器的指令集中,效率
很高但是分配的内容有限.
c.从堆中分配内存:
即是动态分配内存.程序在运行的时候使用malloc/new申请任意大小的内存,程序员
自己负责在何时用free/delete释放内存.动态内存的生存期由程序员决定,使用非常的
灵活.如果在堆上分配了内存,就有责任去回收它,否则运行程序会出现内存泄漏,另外
频繁的分配和释放不同大小的堆空间将会产生堆内碎片.不易管理;
3)堆和栈之间的主要的区别是什么?
a.管理方式不同:栈是由编译器自动分配和释放,使用方便;而对于堆来说,
分配和释放都必须由程序员来手动完成,不易管理,容易
造成内存泄漏和内存碎片.
b.可用内存空间不同:对于栈来说,它可用的内存空间比较小;而对于堆来说它可以使用的空间比栈要大的多.
c.能否产生碎片不同:由于栈采用的是后进先出的机制,所以栈空间没有内存碎片的产生;
而对于堆来说,由于频繁的使用new/delete势必会造成内存空间分配的不连续,从而
造成大量的碎片,使程序的效率降低.
d.生长方向不同: 对于堆来说,它一般是向上的;即是向着地址增加的方向增长;对于栈来说,它一般是向下的,
即向着地址减小的方向增长.
e.分配的方式不同:对于堆来说,它只能是动态分配的;而对于栈来说,它分为静态分配和动态分配;
静态分配由编译器来进行管理,而动态分配的栈和堆也是不一样的,动态分配的
栈由编译器进行释放,无需我们程序员来释放.
f.分配的效率不同:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:为栈分配专门的寄存器.
压栈和出栈都由专门的指令进行.因此它的效率会很高;
而堆则是由c/c++库函数实现的,机制是非常的负责的;例如要分配一块内存的时候,库
函数会利用特定的算法在堆内存中搜索可用大小的内存空间;如果没有足够大的内存
空间,就会调用系统功能去增加数据段的内存空间.这样才能得到足够大的可用的内存
空间,因此堆内存的分配的效率比栈要低得多.
4)new/malloc以及free/delete之间的区别?
a.new/delete是运算符,只能在C++中使用,它可以重载;mallloc/free是C的标准库函数,在C/C++中都可以使用.
b.对于非内部的数据类型的对象而言,光用malloc/free是无法满足动态对象的要求的.对象在创建的时候需要
执行构造函数,对象在消亡之前需要执行析构函数.而molloc/free是库函数而不是运算符,不在编译器的控制
范围之内,编译器不能将执行构造函数和析构函数的任务强加给malloc/free.因此C++需要一个能够完成动态
分配内存和初始化的new,以及一个能够完成清理和释放内存的运算符delete.
c.new的返回值是指定类型的指针,可以自动的计算所需要分配的内存大小.而malloc的返回值是一个void类型
的指针,我们在使用的时候要进行强制类型转换,并且分配的大小也要程序员手动的计算.
d.new/delete完全覆盖了malloc/free的功能,只所以还要保留malloc/free,是因为我们的C++程序有时要调用用C编写的
而C中又没有new/delete,只能使用malloc/free.
3.指针和引用之间的区别和联系?
联系:
a.指针和引用本质上都是地址的概念,引用在内部其实是用const指针来实现的.
b.给函数传递参数的时候,一级指针和引用作为函数参数的时候可以达到相同的效果.
c.指针的大部分效果都可以通过引用来实现。
d.二级指针作为参数的时候就是希望在函数的内部修改指针的指向.这个时候利用指针
的引用可以达到同样的效果.
区别:
a.定义引用的时候必须初始化,定义指针的时候可以不初始化.
b.引用不能引用空,但是指针可以指向空.
c.引用的关系一旦确定,就无法改变;引用永远指向的是用来对它初始化的对象;而非常属性的指针是可以改变指向的.
d.指针是一个实体变量,在32位操作系统上面都是4个字节.而引用只是一个别名,其大小和其应用的对象的类型有关系.
e.有指向指针的指针,但是没有引用引用的引用;因为引用一旦建立,它就表示初始化它的对象.
f.有引用指针的引用,但是没有指向引用的指针;
g.有指针数组,但是没有引用数组,但是有数组的引用.
下面是一段代码非常的全面:
#include <iostream> using namespace std; void foo(int a[3])/*这个地方传递的是数组的首地址*/ { cout << sizeof(a)/sizeof(a[0]) << endl; } void bar(int (&a)[3])/*这里传参的时候就是数组的整体*/ { cout << sizeof(a)/sizeof(a[0]) << endl; } int main(void) { int a; int* p = &a; int** pp = &p;/*存在指向指针的指针*/ int& r = a; int&& rr = r;/*error没有引用引用的引用*/ int*& rp = p; /*有引用指针的引用(指针引用)*/ int&* pr = &r; /*没有指向引用的指针(引用指针)*/ int x, y, z; int* pa[] = {&x,&y,&z};/*指针数组*/ int& ra[] = {x,y,z};/*引用数组是不存在的因为引用不是一个实体*/ int arr[3] = {0}; int (&ar)[3] = arr;/*数组引用(先近后远,先右后左)*/ foo(arr);/*这里传递的是数组的第一个元素的首地址*/ cout << sizeof(arr)/sizeof(arr[0]) << endl; /*这里的数组名代表的是真个数组*/ int (*parr)[3] = &arr;/*对数组名取地址得到的是一个数组指针 这个时候arr代表的是数组的整体;*/ bar(arr);/*这里传递的就是数组的整体*/ return 0; }