C++基础总结
*&表示对指针的引用。
类成员的初始化顺序
由于按成员在类定义中的声明顺序进行构造,而不是按构造函数说明中冒号后面的顺序
::叫作用域区分符,指明一个函数属于哪个类或一个数据属于哪个类。::可以不跟类名,表示全局数据或全局函数(即非成员函数)。
如果 const 出现在 左边,则指针指向的内容为常量;如果 const 出现在 右边,则指针自身为常量;如果 const 出现在 * 两边,则两者都为常量。
effective C++上有个好记的方法:const在号左边修饰的是指针所指的内容;const在号右边修饰的是指针。 简单记就是:左内容,右指针。
const int a;
int const a;
const int *a;
int * const a;
int const * a const;
指针和自增
int a[5] = {1,2,3,4,5};
int *p = a;
cout<<*++p;//out:2 1,2,3
cout<<*p++;//out:1 1,2,3
sizeof总结
参数为数据类型或者为一般变量: 例如sizeof(int),sizeof(long)等等。
参数为数组或指针:※※※※※
int a[50]; //sizeof(a)=4*50=200;求数组所占的空间大小 int *a=new int[50];// sizeof(a)=4; a为一个指针,sizeof(a)是求指针的大小,在32位系统中,当然是占4个字节。
结构体
计算结构变量的大小必须讨论数据对齐的问题。
1. 某些平台只能在特定的地址处访问特定类型的数据;
2. 提高存取数据的速度。比如有的平台每次都是从偶地址处读取数据,对于一个int型的变量,若从偶地址单元处存放,则只需一个读取周期即可读取该变量;但是若从奇地址单元处存放,则需要2个读取周期读取该变量。
3. 静态变量是存放在全局数据区的,而sizeof计算栈中分配的大小,是不会计算在内的)与结构体实例的存储地址无关(注意只有在C++中结构体中才能含有静态数据成员,而C中结构体中是不允许含有静态数据成员的)。
4. 结构体的长度一定是最长的数据元素的整数倍。
另外有几点需要注意: 第一、结构或者类中的静态成员不对结构或者类的大小产生影响,因为静态变量的存储位置与结构或者类的实例地址无关。 第二、没有成员变量的结构或类(非虚)的大小为1,因为必须保证结构或类的每一个实例在内存中都有唯一的地址。 第三、包含虚函数的类或者虚继承的类,需要算上虚表指针的占的4个字节
- new失败返回NULL
const
- 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)
- 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
- 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。
类成员函数的重载、覆盖和隐藏区别?
a.成员函数被重载的特征: (1)相同的范围(在同一个类中);(2)函数名字相同;(3)参数不同;(4)virtual 关键字可有可无。 b.覆盖是指派生类函数覆盖基类函数,特征是: (1)不同的范围(分别位于派生类与基类);(2)函数名字相同;(3)参数相同;(4)基类函数必须有virtual 关键字。 c.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下: (1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。 (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆
在运行时,能根据其类型确认调用哪个函数的能力,称为多态性,或称迟后联编,或滞后联编。编译时就能确定哪个重载函数被调用的,称为先期联编。
复制代码 多态性可可以简单的概括为“一个借口,多种方法”,在程序运行的过程中才决定调用的函数。 虚函数就是允许被其子类重新定义的成员函数。而子类重新定义父类虚函数的做法,称为“覆盖”或“重写”。 覆盖是指子类重新定义父类的虚函数的做法。 重载是指允许存在多个同名函数,而这些函数的参数表不同。 复制代码 为了指明某个成员函数具有多态性,用关键字virtual来标志其为虚函数。 如果虚函数在基类与子类中出现的仅仅是名字的相同,而参数类型不同,或返回类型不同,即使写上了virtual关键字,则也不进行迟后联编。
一个类中将所有的成员函数都尽可能地设置为虚函数总是有益的。它除了会增加一些资源开销,没有其它坏处。
设置虚函数,需注意下列事项: 只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象,所以普通函数不能说明为虚函数。 静态成员函数不能是虚函数,因为静态成员函数不受限于某个对象。 内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时,仍将其看作非内联的。 构造函数不能是虚函数,因为构造时,对象还是一片未定型的空间。只有在构造完成后,对象才能成为一个类的名副其实的实例。 析构函数可以是虚函数,而且通常声明为虚函数
三十一、lambda表达式
一个lambda表达式表示一个可调用的代码单元,我们可以将其理解为一个未命名的内联函数。与任何函数相似,一个lambda具有一个返回类型、一个参数列表、一个函数体。但与函数不同,lambda可以定义在函数内部,一个lambda表达式具有如下形式:
capture list捕获列表 {函数体},例子如下:
auto f = {return a.size() < b.size()} 我们可以忽略参数列表和返回类型,但永远包含捕获列表和函数体,其中捕获列表内容通常为空。
二十八、拷贝构造函数为什么传引用?
原因:参数为引用,不为值传递是为了防止拷贝构造函数的无限递归,最终导致栈溢出。这也是编译器的强制要求。
二十九、程序崩溃原因
- 读取未赋值的变量
- 函数栈溢出
- 数组越界访问
- 指针的目标对象不可用
二十二、C/C++内存管理方式,内存分配
内存分配方式:在C++中内存分为5个区,分别是堆、栈、自由存储区、全局/静态存储区、常量存储区。
- 栈:在执行程序过程中,局部作用域出现的一些局部变量可以在栈上创建,等脱离该作用域创建的内存被释放。栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
- 堆:用于程序内存动态分配,用c/c++中的new/malloc分配,delete/free释放。堆则是 C/C++ 函数库提供的,它的机制是很复杂的。
- 自由存储区:它是C++基于new操作符的一个概念,凡是通过new操作符申请的内存即为自由存储区
- 全局/静态存储区:这块内存在程序编译期间已经分配好,在程序整个运行阶段一直存在。全局变量个和静态变量
- 常量存储区:特殊的一块内存,里面存放的是常量,不允许修改。
十九、struct内存大小的确定
存在内存对齐的缘故,对于32位机器,是4字节对齐,64位机器是8字节对齐。
十二、static
- 局部静态变量:static局部变量和普通局部变量有什么区别?
- 生存期不同
- static局部变量和普通局部变量有什么区别:static局部变量只被初始化一次,下一次依据上一次结果值
- 程序的局部变量存在于(堆栈)中,全局变量存在于(静态区 )中,动态申请数据存在于( 堆)中。
- 生存期不同
- 全局静态变量:static全局变量与普通的全局变量有什么区别?
- 作用域不同
- 非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。
- 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。
- static全局变量只初使化一次,防止在其他文件单元中被引用;可以避免在其它源文件中引起错误。
- 作用域不同
一个C++源文件从文本到可执行文件经历的过程
- 预处理:对所有的define进行宏替换;处理所有的条件编译#idef等;处理#include指令;删除注释等;bao#pragma
- 编译:将预处理后的文件进行词法分析、语法分析、语义分析以及优化相应的汇编文件
- 优化:
- 汇编:将汇编文件转换成机器能执行的代码
- 链接:包括地址和空间分配,符号决议和重定位
十、malloc的原理
函数原型: void malloc(size_t n)返回值额类型为void,为动态分配得到的内存,但代大小是确定的,不允许越界使用。
malloc函数的实质
- 它有一个可以将可用内存块连接成一个长的列表的空闲链表
- 当调用链表时,它沿着连接表寻找一个大到可以满足用户请求所需内存
- 将内存一分为二,将分配给用户那块内存传给用户,剩下的那块返回连接表。
sizeof和strlen的区别:
- sizeof: 返回一个变量或者类型的字节大小,占用的空间大小,不管它第几个是\0
- strlen: 求字符串的长度,也就是从第一个字符到第一个\0的距离
1. static(静态)变量有什么作用
个体明显的作用:
- 只初始化一次:在一个函数被调用的过程中其值维持不变,一直都没有被销毁,下一次的运算依据是上一次的结果值。目的是为了防止在其他文件单元中被引用。
- 作用域范围是有限:即如果一个变量被声明为静态的,那么该变量只可被模块内所有函数访问
- static函数只在一个源文件中有效,不能被其他源文件使用。
#include<stdio.h>
void fun(int i)
{
static int value=i++;
value++;
printf("%d\n",value);
}
int main()
{
fun(0);
fun(3);
fun(5);
return 0;
}
//程序输出:
1
2
3
static int value=i++这个定义语句只会在第一次调用的时候执行
在头文件中定义静态变量,是否可行?为什么?
不可行,如果在头文件中定义静态变量,会造成资源浪费的问题,同时也可能引起程序错误。
- 因为如果在使用了这个头文件的每个C 语言文件中定义静态变量,按照编译的步骤,在每个头文件中都存在一个静态变量,从而会引起空间浪费和程序错误。
- 所以不推荐在头文件中定义任何变量,当然也包括静态变量。
2. const有哪些作用:
- 定义const常量,具有不可变性
- 保护被修饰的东西
- 进行类型检查
- 方便进行参数的调整和修改
- 为函数重载提供参考
- 节省空间,避免不必要的内存分配
- 提高了程序的效率。编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译器间的常量,没有了存储与读内存的操作,使得它的效率也很高。
为什么要使用const引用?
NOTE
- 一般引用初始化一个左值的时候,没有任何问题
- 而当初始化值不是一个左值时,则只能对一个常引用赋值
而且这个赋值是有一个过程的
- 首先将值隐式转换到类型T
- 然后将这个转换结果存放在一个临时对象里
- 最后用这个临时对象来初始化这个引用变量。
const引用可以初始化为不同类型的对象或者初始化为右值,如字面值常量
- 而非const引用只能绑定到该引用同类型的对象。
3. volatile在程序设计中有什么作用?
NOTE
volatile 是一个修饰符,它用来修饰被不同线程访问和修改的变量。被volatile类型定义的变量,系统每次用到它的时候都是直接从对应的内存当中提取,而不会利用cache中的原有数值,以适应它的未知何时会发生的变化,系统对这种变量的处理不会做优化。所以,volatile一般用于修饰多线程间被多个任务共享的变量和并行设备硬件寄存器等。
4.char str[]="abc";char str2[]="abc";str1与str2不相等,为什么?
两者不相等,是因为str1和str2都是字符数组,每个都有其自己的存储区,它们的值则是各存储区的首地址。
const char *str3="abc"和const char *str4="abc"相等 str3和str4是字符指针而非字符数组,并不分配内存,其后的“abc”存放在 *常量区,str3和str4是指向它们指向的地址的首地址,而它们自己仅是指向该区首地址的指针,所以相等。
5. C++里面是不是所有的动作都是main()函数引起的,但是一个C语言程序总是从main()函数开始执行的。
不是,对于C++程序而言, 静态变量、全局变量、全局对象的分配早在main()函数之前已经完成 所以并不是所有的动作都是main()引起的,只是编译器是由main()开始执行的,main()只不过是一个约定的函数入口,在main()函数中的显示代码之前,会调用一个由编译器生成的_main()函数,而_main()函数会进行所有全局对象的构造及初始化工作。
在main()函数退出后再执行一段代码? 答案依然是全局对象, 当程序退出后,全局变量必须销毁,自然会调用全局对象的析构函数,所以剩下的就同构造函数一样了。
6. 前置运算和后置元素有什么区别?
- 以++操作为例,对于变量a,++a表示取a的地址,增加它的内容,然后把值放在寄存器中
- a++表示取a的地址,把它的值放入寄存器中,然后增加内存中a的值
- 前置(++)通常要比后置自增(—++)效率更高。
例题:a是变量,执行(a++)+=a语句是否合法?
左值和右值的概念
- 左值就是可以出现在表达式左边的值(等号左边),可以被改变,它是存储数据值的那块内存的地址,也称为变量的地址
- 右值是指存储在某内存地址中的数据,也称为变量的数据。
- 左值可以作为右值,但是右值不可以是左值。
本题不合法 a++不能当做左值使用。++a可以当作左值使用。++a表示取a的地址,对它的内容进行加1操作,然后把值放在寄存器中。a++表示取a的地址,把它的值装入寄存器,然后对内存中a的值执行加1操作。
7. new/delete与malloc/free的区别是什么?
- new能够自动计算需要分配的内存空间,而malloc需要手工计算字节数。
- new和delete直接带具体类型的指针,malloc和free返回void的指针。
- new是类型安全的,而malloc不是。
- new一般由两步构成,分别是new操作和构造。new操作对应于malloc,但new操作可以重载,可以自定义内存分配策略,不做内存分配,甚至分配到非内存设备上,而malloc不行。
- new将调用构造函数,而malloc不能;delete将调用析构函数,而free不能。
- malloc/free需要库函数stdlib.h的支持,而new/delete不需要
注意资源泄漏的问题
- new/delete,malloc/free必须配对使用。
- 并且释放完内存后,应该将指针指向NULL。
- 因为仅仅告诉操作系统存储指针的内存已经释放了,可以做其他用途,但指针的值还没有被立刻清空
8. 已知String类定义,如何实现其函数体。
String类定义如下:
class String{
public:
String(const char* str=NULL);
String(const String &another);
~String();
String &operator=(const String &rhs);
private:
char* m_data;
};
String::String(const char *str)
{
if(str==NULL)
{
m_data=new char[1];
m_data[0]='\0';
}
else
{
m_data=new char[strlen(str)+1];
strcpy(m_data,str);
}
}
String::String(const String &another)
{
m_data=new char[strlen(another.m_data)+1];
strcpy(m_data,another.m_data);
}
String::~String()
{
delete[] m_data;
}
String& String::operator=(const String &rhs)
{
if(this==&rhs)
return *this;
delete[] m_data;
m_data=new char[strlen(rhs.m_data)+1];
strcpy(m_data,rhs.m_data);
return *this;
}
9. 栈空间的最大值是多少?
在Windows,栈是向低地址扩展的数据结构,是一块连续的内存的区域。栈顶的地址和栈的最大容量是系统预先规定好的 在Windows下,栈的大小是2MB。而申请堆空间的大小一般小于2GB.
- 栈的速度快,但是空间小,不灵活。
- 堆获得的空间比较灵活,也比较大,但是速度相对慢一些。
由于内存的读取速度比硬盘快,当程序遇到大规模数据的频繁存取时,开辟内存空间很有作用。栈的速度快,但是空间小,不灵活。堆是向高地址扩展的,是不连续的内存区域。这是由于系统是用链表来存储空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址的,而堆的大小受限于计算机系统中的有效虚拟内存,所以堆获得的空间比较灵活,也比较大,但是速度相对慢一些。
10. 指针和引用的区别
指针指向一块内存,它的内容是所指内存的地址,引用是某块内存的别名
- 从本质上讲
- 指针是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,即其所指向的地址可以被改变,其指向的地址中所存放的数据也可以被改变。
- 而引用则只是一个别名而已,它在逻辑上不是独立的,它的存在具有依赖性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的,即自始自终只能依赖于同一个变量,具有“从一而终”的特性。
- 作为参数传递时
- 指针传递参数本质上是值传递的方式,它所传递的是一个地址值(所有对形参的改变都只是这个地址值中存放变量的改变,而存放这个地址值的指针是不会变化的。如果要改变存放该地址值的指针,需要传入的是该指针的地址,所以可以使用指针的指针或者指针的引用。)。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值。
- 而在引用传递过程中,被调用函数的形参虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。对引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。
- 引用使用时不需要解引用(*),而指针需要解引用。
- 引用只能在定义时被初始化一次,之后不能被改变,即引用具有“从一而终”的特性,而指针却是可以改变的。
- 引用不可以为空,而指针可以为空。引用必须与存储单元相对应,一个引用对应一个存储单元。
- 对引用进行sizeof操作得到的是所指向的变量(对象)的大小,而对指针进行sizeof操作得到的是指针本身(所指向的变量或对象的地址)的大小。
- 指针和引用的自增(++)运算意义不一样。
- 如果返回动态分配的对象或内存,必须使用指针,引用可能引起内存泄漏。
11. 指针和数组是否表示同一概念
主要表现在以下两方面的不同:
- 修改内容不同。
- 例如,char a[]="hello",可以通过去下标的方式对其进行修改,而对于char *p="word",此时p指向常量字符串,所以p[0]='x'是不允许的。
- 所占字节数不同
- 例如,char p="world",p为指针,则sizeof(p)得到的是一个指针变量的字节数,而不是p所指的内存容量。 char a[]="hello world"; char \p=a; 在32位机器上,sizeof(a)=12字节,而sizeof(p)=4字节。 但要注意的是,当数组作为函数参数进行传递时,该数组自动退化为同类型的指针。
13. 野指针?空指针?
野指针是指指向不可用内存的指针。
- 任何指针变量在被创建时,不会自动成为NULL指针(空指针),其默认值是随机的,所以指针变量在创建的同时应当被初始化,或者将指针设置为NULL,或者让它指向合法的内存,而不应该放之不理,否则就会称为野指针。
- 指针被释放(free或delete)后,未能将其设置为NULL,也会导致该指针变为野指针。
- 指针操作超越了变量的作用范围。
14. #include和#include"filename.h"有什么区别
对于#include<filename.h>
- 编译器先从标准库路径开始搜索filename.h,然后从本地目录搜索,使得系统文件调用较快。 而对于#include"filename.h"
- 编译器先从用户的工作路径开始搜索filename.h,后去寻找系统路径,使得自定义文件较快。
15. 宏的总结
宏与函数的区别
- 函数调用时,首先求出实参表达式的值,然后带入形参。而使用带参数的宏只是进行简单的字符替换
- 函数调用在程序运行时处理的,它需要分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,也不进行值的传递处理,也没有“返回值”的概念。
- 对函数中的实参和形参都有定义类型,两者的类型要求一致。而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。
- 调用函数只可能得到一个返回值,而用宏可以设法得到几个结果。
- 使用宏次数多时,宏展开后源程序会变很长,因为每展开一次都是程序内容增长,而函数调用不会使源程序变长。
- 宏替换不占用运行时间,而函数调用则占用运行时间
- 参数每次用于宏定义时,它们都要重新求值,由于多次求值,具有副作用的参数可能会产生不可预料的结果。
枚举和define有什么不同
- 枚举常量是实体中的一种,而宏定义不是实体
- 枚举常量属于常量,而宏定义不是常量
- 枚举常量具有类型,但宏没有类型,枚举变量具有与普遍变量相同的性质,如作用域、值等,但宏没有。
- define宏常量是在预编译阶段进行简单替换,枚举常量则是在编译的时候确定其值。
- 一般在编译器里,可以调试枚举常量,但是不能调试宏常量
- 枚举可以一次定义大量相关的常量,而#define宏一次只能定义一个
typedef和define的区别
- 原理不同。#define是C语言中定义的语法,它是预处理指令,在预处理时进行简单的字符替换,不作正确性检查。typedef是关键字,它在编译时处理,所以typedef有类型检查的功能
- 功能不能。typedef用来定义类型的别名,这些类型可以是内置类型也可以是用户自定义的类型。#define不只是可以为类型去名字,还可以定义常量、变量、编译开关
- 作用域不同。#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而typedef有自己的作用域
- 对指针的操作不同。两者修饰指针类型时,作用不同。
宏定义与inline函数的区别
- 宏定义是在预处理阶段进行代码替换,而内联函数是在编译阶段插入代码;
- 宏定义没有类型检查,而内联函数有类型检查。
define和const的区别
- define只是用来进行单纯的文本替换,不分配内存空间,而const常量存在于程序的数据段,并在堆栈中分配了空间
- const常量有数据类型,而define常量没有数据类型
- 很多IDE支持调试const定义的常量,而不支持define定义的常量
16. C语言中struct和union的区别是什么
union | struct |
---|---|
公用同一个内存地址空间 | 不同成员会存在不同的地址。 |
不同成员赋值会对其他成员重写 | 不同成员赋值是互不影响的 |
C语言和C++中struct的区别
C | Cpp |
---|---|
不能有函数成员 | struct可以有 |
数据成员没有private、public和protected访问权限的设定 | struct成员有访问权限限定。 |
没有继承关系的 | struct有丰富的继承关系 |
C++中struct和class的区别
C++class | C++struct |
---|---|
默认是private的 | struct默认是public的 |
可以用于定义模板,就像typename | struct不可以 |
17. 位运算总结
- 如何快速求取一个整数的7倍? (X<<3)-X;
- 如何实现位操作求两个数的平均值 一般而言,求平均值可以使用(x+y)>>1,但是x+y可能移除,所以不使用加入,而使用异或和与运算实现加法: (x&y)+(x^y)>>1
- 如何利用位运算计算数的绝对值? 以x为负数为例来分析,因为在计算机中,数字都是以补码的形式存在的,求负数的绝对值,应该是不管符号位,执行按位求反,末尾加1操作即可。 对于一个负数,将其右移31位后会变成0xffffffff,而对于一个正数而言,右移31位则为0x00000000,而0xffffffff^x+x=-1,因为任何数与1111异或,其实质都是把x的0和1进行颠倒计算。如果用变量y表示x右移31为,则(x^y)-y则表示的是x的绝对值。
18. 考虑n个二进制组成的数中,有多少个数中不存在两个相邻的1.
当n=1时,满足条件的二进制数为0、1,一共两个数;当n=2时,满足条件的二进制数有00、01、10,一共3个数;当n=3时,满足条件的二进制数有000、001、010、100、101,一共5个数。对n位二进制数,设所求结果a(n),对于第n位的值,分为0或者1两种情况:
- 第n位为0,则有a(n-1)个数。
- 第n位为1,则要满足没有相邻万为1的条件,第n-1位为0,有a(n-2)个数,因此得出结论a(n)=a(n-1)+a(n-2) 满足斐波拉契数列。
19. 函数指针和指针函数的区别
指针函数是指带指针的函数,本质上是一个函数,函数返回类型是某一类型的指针。其形式一般如下所示: 类型标识符 函数名(参数列表) 例如,int f(x,y),它的意思是声明一个函数f(x,y),该函数返回类型为int型指针。 而函数指针是指向函数的指针,即本质是一个指针变量,表示的是一个指针,它指向的是一个函数。其形式一般如下所示: 类型说明符 (函数名)(参数) 例如,int (pf)(int x)它的意思就是声明一个函数指针,而pf=func则是将func函数的首地址赋值给指针。 引申:
- 数组指针/指针数组 数组指针就是指向数组的指针,它表示的是一个指针,它指向的是一个数组,它的重点是指针。例如,int(pa)[8]声明了一个指针,该指针指向了一个有8个int型元素的数组。数组指针类似于二维i数组。即int a[][8]; 指针数组就是指针的数组,表示的是一个数组,它包含的元素是指针,它的重点是数组。例如,int ap[8]声明了一个数组,该数组的每一个元素都是int型的指针。
- 函数模板/模板函数 函数模板是对一批模样相同的函数的说明描述,它不是某一具体的函数;而模板函数则是将函数模板内的“数据类型参数”具体化得到的重载函数(就是由模板而来的函数简单地说,函数模板是抽象的,而模板函数则是具体的。 函数模板减少了程序员输入代码的工作量,是C++中功能最强的特性之一,是提高软件代码重用率的重要手段之一。函数模板的形式一般如下所示: template\<模板类型形参表> \<返回值类型> \<函数名>(模板函数形参表) { //函数体 } 其中\<模板函数形参表>的类型可以是任何类型。需要注意的是,函数模板并不是一个实实在在的函数,它是一组函数的描述,它并不能直接执行,需要实例化成模板函数后才能执行,而一旦数据类型形参实例化以后,就会产生一个实实在在的模板函数了。
- 类模板/模板类 类模板与函数模板类似,将数据类型定义为参数,描述了代码类似的部分类的集合,具体化为模板类后,可以用于生存具体的对象。 template<类型参数表> class<类名> { //类说明体 }; template<类型形参表> <返回类型><类名><类型名表>::<成员函数1>(形参表) { //成员函数定义体 } 其中<类型形参表>与函数模板中的一样,而类模板本身不是一个真实的类,只是对类的一种描述,必须用类型参数将其实例化为模板类后,才能用来生成具体的对象。简而言之,类是对象的抽象,而类模板就是类的抽象。 C++中引入模板类主要有以下5个方面的好处:
- 可用来创建动态增长和减少的数据结构
- 它是类型无关的,因此具有很高的可复用性
- 它在编译时而不是运行时检查数据类型,保证了类型安全
- 它是平台无关的,可移植性强
- 可用于基本数据类型
- 指针常量/常量指针
指针常量是指定义的指针只能在定义的时候初始化,之后不能改变其值。其格式为:
[数据类型][*][const][指针常量名称]
例如:char const p1; int \const p2;(顶层const)
常量指针的值不能改变,但是其指向的内容却可以改变。
常量指针是指指向常量的指针,因为常量指针指向的对象是常量,因此这个对象的值是不能够改变的。定义的格式如下:
[数据类型][const][*][常量指针名称];
例如,int const *p; const int *p;
需要注意的是,指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变型,它所指向的对象的值是不能通过常量指针来改变的。
20. C++函数传递参数的方式有哪些
- 值传递 当进行值传递时,就是将实参的值复制到形参中,而形参和实参不是同一个存储单元,所以函数调用结束后,实参的值不会改变。
- 指针传递(实际上指针的值还是没有改变的,改变的只是指针中存放的地址所指向的变量,如果要改变指针的值,需要传递指针的引用或者指向指针的指针) 当进行指针传递时,形参是指针变量,实参是一个变量的地址,调用函数时,形参(指针变量)指向实参变量单元。这种方式还是“值传递”,只不过实参的值是变量的地址而已。而在函数中改变的不是实参的值,而是实参中存放的地址所指向的变量的值。
- 传引用
实参地址传递到形参,使形参的地址取实参的地址,从而使形参与实参共享同一单元的方式。
21. 重载与覆盖有什么区别?
22. 是否可以通过绝对内存地址进行参数赋值与函数调用
23. 默认构造函数是否可以调用单参数构造函数
默认构造函数不可以调用单参数的构造函数。 例如:
此时i的值是未定义的。以上代码希望默认构造函数调用带参数的构造函数,可是却未能实现。因为在默认构造函数内部调用带餐的构造函数属于用户的行为而非编译器行为,它只执行函数调用,而不会执行其后的初始化表达式。只有生成对象时,初始化表达式才会随相应的构造函数一起调用。 可以使用委托构造函数class A { public: A() { A(0); print(); } A(int j):i(j) { cout<<"call A(int j)"<<endl; } void print() { cout<<"call print()"<<endl; } int i; };
class A { public: A():A(0) { print(); } A(int j):i(j) { cout<<"call A(int j)"<<endl; } void print() { cout<<"call print()"<<endl; } int i; }; 或者使用placement new class A { public: A() { printf("In A::(). m_x=%d\n", m_x); new(this) A(0); printf("Out A::(). m_x=%d\n", m_x); } A(int x) { printf("In A::(int x). x=%d\n", x); m_x=x; } private: int m_x; };
25. C语言中各种变量的默认初始值是什么?
全局变量放在内存的全局数据区,由编译器建立,如果在定义的时候不做初始化,则系统将自动为其初始化,数值型为0,字符型为NULL,即0,指针数组也被赋值为NULL。静态变量的情况与全局变量类似。而非静态局部变量如果不显示初始化,那么其内容是不可预料的,将是随机数,会很危险。
28. 面向对象的基本特征:
封装是指将客观事物抽象成类,每个类有自己的数据和行为实现保护。 继承可以使用现有类的所有功能,而不需要重新编写原来的类,它的目的是为了进行代码复用和支持多态。 多态是指同一个实体同时具有多种形式,它主要体现在类的继承体系中,它是将父对象设置成为一个或更多的它的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
29. 复制构造函数与赋值运算符的区别是什么?
主要有以下3个方面的不同:
- 复制构造函数生成新的类对象,而赋值运算符不能。
- 由于复制构造函数是直接构造一个新的类对象,所以在初始化这个对象之前就不用检验源对象是否和新建对象相同。而赋值运算符总则需要这个操作,另外赋值运算符中如果原来的对象中有内存分配,要先把内存释放掉。
- 当类中有指针类型的成员变量时,一定要重写复制构造函数和赋值构造函数,不能使用默认的。
30. 基类的构造函数/析构函数是否能被派生类继承
基类的构造函数/析构函数不能被派生类继承。 基类的构造函数不能被派生类继承,派生类中需要声明自己的构造函数。在设计派生类的构造函数时,不仅要考虑派生类所增加的数据成员初始化,也要考虑基类的数据成员的初始化。声明构造函数时,只需要对本类中新增成员进行初始化,对继承来的基类成员的初始化,需要调用基类构造函数完成。 基类的析构函数也不能被派生类继承,派生类需要自行声明析构函数。声明方法与一般类的析构函数一样,不需要显式地调用基类的析构函数,系统会自动隐式调用。需要注意的是,析构函数的调用次序与构造函数相反。
31. 初始化列表和构造函数初始化的区别是什么?
初始化列表的一般形式如下:
Object::Object(int _x,int _y):x(_x),y(_y) {}
构造函数初始化一般通过构造函数实现,实现如下:
Object::Object(int _x,int _y)
{
x=_x;
y=_y;
}
- 上面的构造函数使用初始化列表的会显式地初始化类的成员;而没有使用初始化列表的构造函数是对类的成员赋值,并没有进行显式的初始化。
- 初始化和赋值对内置类型的成员没有什么的的区别,在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的。对非内置类型成员变量,因为类类型的数据成员的数据成员对象在进入函数体前已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,调用构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用一个赋值赋值操作符才能完成(如果并未提供,则使用编译器提供的默认成员赋值行为)。为了避免两次构造,推荐使用类构造函数初始化列表。
- 但有很多场合必须使用带有初始化列表的构造函数。例如,成员类型是没有默认构造函数的类,若没有提供显示初始化时,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试调用默认构造函数将会失败。再例如const成员或者引用类型的成员,因为const对象或引用类型只能初始化,不能对它们进行赋值。
32. 类的成员变量的初始化顺序
变量的初始化顺序:
- 基类的 静态变量或全局变量。
- 派生类的静态变量或全局变量
- 基类的成员变量
- 派生类的成员变量 构造的顺序: 虚基类的构造函数->一般基类构造函数的调用(根据声明的次序调用每一个基类的构造函数)->如果存在虚函数表,设定vptr的值->对构造函数初始列表中的其他成员进行构造->如果存在对象成员分别调用其构造函数进行构造->初始化列表中的成员按照其在类中的声明次序进行构造->执行构造函数体内的代码
34. 构造函数没有返回值,那么如何得知对象是否构造成功?
这里的“构造”不是单指分配对象本身的内存,而是指建立对象时做的初始化(如打开文件、连接数据库) 因为构造函数没有返回值,所以通知对象的构造失败的唯一方法就是在构造函数中抛出异常。构造函数中抛出异常将导致对象的析构函数不被执行,但对象发生部分构造时,已经构造完毕的子对象将会逆序地被析构。
35. C++中的空类默认产生哪些成员函数
C++中空类默认会产生以下6个函数:*
- 默认构造函数
- 复制构造函数
- 析构函数
- 赋值运算符重载函数
- 取址运算符重载函数
- const取址运算符重载函数等。
class Empty { public: Empty();//默认构造函数 Empty(const Empty&);//复制构造函数 ~Empty(); //析构函数 Empyt& operator=(const Empty&); //赋值运算符 Empty* operator&(); //取址运算符 const Empty* operator&() const;//取址运算符const };
C++提供默认值参数的函数
注意: - 如果一个函数中有过个默认值,则形参分布中,默认参数应从右至左逐渐定义。
- 在默认参数调用时,调用顺序为从左至右逐个调用
- 默认值可以是全局变量、全局常量,甚至可以是一个函数,默认值不能是局部变量
- 默认参数可将一系列简单的重载函数合成为一个
36. 实现多态的基本原理
应在构造函数中实现虚函数表的创建和虚函数指针的初始化。根据构造函数的调用顺序,在构造子类对象时,先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否还有继承,它初始化父类对象的虚函数表的指针,该虚函数表指针指向父类的虚函数表。当执行子类的构造函数时,子类对象的虚函数表指针被初始化,指向自身的虚函数表。 编译器发现一个类中有虚函数,便会立即为此类生成虚函数表,虚函数表的各表项为指向对应虚函数的指针。编译器还会在此类中隐含插入一个vptr指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行vptr与vtable的关联代码,将vptr指向对应的vtable,将类与此类的vtable联系起来,另外在调用类的构造函数时,指向基础类的指针此时已经变成指向具体类的this指针,这样依靠此this指针即可得到正确的vatble。这样才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。37. C++中的多态种类有哪几种?
C++中的多态包括参数多态、引用多态、过载多态和强制多态等。 参数多态是指采用参数化模板,通过给定不同的类型参数,使得一个结构有多种类型、模板。 引用多态是指同样的操作可以用于一个类型及其子类型。 过载多态是指同一个名字在不同的上下文中有不同的类型。 强制多态则是指把操作对象的类型强加以变换,以符合或操作符的要求。38. 什么函数不能声明为virtual
有5种情况: - 只有类的成员函数才能说明为虚函数
- 静态成员不能为虚函数,因为调用静态成员函数不要实例,但调用虚函数需要从一个实例中指向虚函数表的指针以得到函数的地址,因此调用虚函数需要一个实例,两者互相矛盾。
- 内联函数不能为虚函数
- 构造函数不能为虚函数
- 析构函数可以为虚函数,而且通常声明为虚函数
构造函数不能是虚函数,是因为构造函数是在对象完全构造之前运行的,换句话说,运行构造函数前,对象还没有生成,更谈不上动态类型了。构造函数是初始化虚表指针,而虚函数放在虚表里面,当要调用虚函数的时候首先要知道虚表指针,这个就是矛盾的地方了,所以构造函数不可能是虚函数。一般上,构造函数是不能调用虚函数,但是在构造函数中还是可以调用虚函数,只是此时的虚函数不会表现动态类型,而只是静态类型。
39. 是否可以把每个函数都声明为虚函数
虽然虚函数很有效,但是不能把每个函数都声明为虚函数。因为使用虚函数是要付出代价的。由于每个虚函数的对象在内存中都必须维护一个虚函数表指针,因此在使用虚函数时,尽管带来了方便,却会额外产生一个系统开销。40. C++如何阻止一个类被实例化
- 使用抽象类
- 将构造函数声明为private
41. C++哪些函数只能使用成员初始化列表而不能使用赋值。
在C++赋值与初始化列表的情况不一样,只能用初始化列表而不能用赋值的情况一般有一下3种: - 当类中含有const(常量)、reference(引用)成员变量时,只能初始化不能对他们进行赋值。常量不能被赋值,只能被初始化,所以必须在初始化列表中完成,C++的引用也一定要初始化,所以必须在初始化列表中完成。
- 基类的构造函数都需要初始化列表。构造函数的意思是先开辟空间然后为其赋值,只能算是赋值,不算初始化。
- 成员类型是没有默认构造函数的类。若没有提供显式初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
42. 虚函数
指向基类的指针在操作它的多态类对象时,会根据不同的类对象调用其相应的函数,这个函数就是虚函数。虚函数使用virtual修饰函数名。虚函数的作用是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数进行重新定义。在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型,以实现统一的接口。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。 在使用虚函数时要注意以下几方面: - 只需要在声明函数的类体中使用关键字virtual将函数声明为虚函数,而定义函数时不需要使用关键字virtual。
- 当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数自动成为虚函数。
- 如果声明了某个成员函数为虚函数,则在该类中不能再出现与这个成员函数同名并返回值、参数个数、类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种同名函数。
- 非类的成员函数不能定义为虚函数,全局函数以及类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以讲析构函数定义为虚函数。将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。
- 普通派生类对象,先调用基类构造函数再调用派生类构造。
- 基类的析构函数应该定义为虚函数,这样可以在实现多态的时候不造成内存泄露。基类析构函数未声明virtual,基类指针指向派生类时,delete指针不调用派生类析构函数。有virtual,则先调用派生类析构函数再调用基类析构。
- 基类指针动态建立派生类对象,普通调用派生类构造函数
- 指针声明不调用构造函数。
43. 写出float x 与“零值”比较的if语句
写出float x 与“零值”比较的if语句 请写出 float x 与“零值”比较的 if 语句:const float EPSINON = 0.00001; if ((x >= - EPSINON) && (x <= EPSINON) //不可将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”此类形式。 //EPSINON应该是一个很小的值吧 因为计算机在处理浮点数的时候是有误差的,所以判断两个浮点数是不是相同,是要判断是不是落在同一个区间的,这个区间就是 [-EPSINON,EPSINON] EPSINON一般很小,10的-6次方以下吧,具体的好像不确定的,和机器有关。
24. 什么是可重入函数?C语言如何写可重入函数
可重入函数是指能够被多个线程“同时”调用的函数,并且能保证函数结果正确性的函数。 在C语言中编写可重入函数时,尽量不要使用全局变量或静态变量,如果使用了全局变量或静态变量,就需要特别注意对这类变量访问的互斥。一般采用以下几种措施来保证函数的可重入性:信号量机制、关调度机制、关中断机制等方式。 biaozhunk 需要注意的是,不可调用不可重入函数,当调用了不可重入的函数时,会使该函数也变为不可重入的函数。一般驱动程序都是不可重入的函数,因此在编写驱动程序时一定要注意重入的问题。26. 编译和链接的区别
27. 编译型语言和解释性语言的区别
33. C++能设计实现一个不能被继承的类
C++不同于Java,Java中被final关键字修饰的类不能被继承。C++能实现不能继承的类,但是需要自己实现。 为了使类不被继承,最好的办法是使子类不能构造父类的部分,此时子类就无法实例化整个子类。在C++中,子类的构造函数会自动调用父类的构造函数,子类的析构函数也会自动调用父类的析构函数,所以只要把类的构造函数和析构函数都定义为private函数,那么当一个类试图从它那儿继承时,必然会由于试图调用构造函数、析构函数而导致编译错误,此时该类不能被继承。 可是这个类的构造函数和析构函数都是私有函数了,我们怎样才能得到该类的实例呢?这难不倒我们,我们可以通过定义静态来创建和释放类的实例。 基于这个思路,我们可以写出如下的代码:
class FinalClass1
{
public :
static FinalClass1* GetInstance()
{
return new FinalClass1;
}
static void DeleteInstance( FinalClass1* pInstance)
{
delete pInstance;
pInstance = 0;
}
private :
FinalClass1() {}
~FinalClass1() {}
};
//这个类在基本上就能实现不能继承的功能。但是每次如果你都用这样一个类的话,估计你到最后不是你的程序崩溃了,而是你自己崩溃的更早。
//因此,我们这样设计。
class CFinalClassMixin
{
friend class CParent;
private:
CFinalClassMixin(){}
~CFinalClassMixin(){}
};
class CParent: public CFinalClassMixin
{
public:
CParent(){}
~CParent(){}
};
class CChild : public CParent
{
};
//但是发现没有用,想一想也是,CChild构造函数调用CParent的构造函数,而CParent的构造函数再调用CFinalClassMixin的构造函数,很显然是合法的。
//我估计你也想骂了,唧唧歪歪讲了这么就还是不行。
//但是请你想想,如果我是在CChild的构造函数直接调用CFinalClassMixin的构造函数,而CFinalClassMixin的构造函数是private,不能被调用,那我们岂不是达到了目的,但是我们如何才能在CChild中直接调用CFinalClassMixin的构造函数而不是通过CParent去调用了。
//给你一分钟去想想。。。。。。。。。。。。。。
//哈哈虚继承,虚继承刚好可以实现上述目的。
//因此:
class CFinalClassMixin
{
friend class CParent;
private:
CFinalClassMixin(){}
~CFinalClassMixin(){}
};
class CParent: virtual public CFinalClassMixin
{
public:
CParent(){}
~CParent(){}
};