C++基础
C++基础
第一章、概述
1、在学习C++编程前,首先来重复一个基本的问题:程序由什么组成、算法的5大特征、以及面向对象的5大原则?
答:程序=数据结构+算法
算法的5个基本特征:确定性、有穷性、输入、输出、可行性。
确定性:算法的每一步骤必须有确切的定义;
有穷性:算法的有穷性是指算法必须能在执行有限个步骤之后终止;
输入:一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定出了初始条件;
输出:一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
可行性:算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步,即每个计算步都可以在有限时间内完成;
面向对象的5大原则:单一职责原则(SRP)、开放封闭原则(OCP) 、里氏替换原则(LSP)、依赖倒置原则(DIP) 、接口隔离原则(ISP);
2、C++不是类型安全的
答:C++ 是类型不安全的,C#和java是类型安全的。
对于C++类型不安全举个例子:C++中可以直接将本应返回bool型的函数返回int,然后由编译器自己将int转化为bool型(非零转化为true,零转化
false)。注意:类型安全就是指两个类型直接要相互转换,必须要显示的转换,不能隐式的只用一个等于号就转换了。
补充:①string及STL模板库是类型安全的;②MFC中CString是类型安全的类,其中所有类型转换必须显示转换;
3、C++中常见的关键字含义
答:如下:
①inline:定义内联函数,该关键字是基于定义,如果只在函数声明时给出inline,则函数不会被认为是内联函数,所以必须在函数定义的地方也加上inline,同时inline只是向编译器建议函数以内联函数处理,不是强制的;
②const:定义常成员,包括const数据成员和const成员函数,const数据成员必须,也只能通过构造函数的初始化列表进行初始化,const成员函数只能访问类的成员,不能进行修改,如果需要修改,则引入下面的mutable关键字;
③mutable:这个关键字的引入是解决const成员函数要修改成员变量,通常而言,const成员函数只能访问成员变量,不能修改,但是如果成员变量被mutable修饰了,则在const成员函数中可以修改该变量。mutable和const不能同时用于修饰成员变量;
④ static:声明静态成员,包括静态数据成员和静态成员函数,它们被类的所有对象共享,静态数据成员在使用前必须初始化,而静态成员函数只能访问静态数据成员,不能访问非静态数据成员,因为该函数不含有this指针;
static成员函数不可以访问非静态成员的详细解释:
普通的非静态成员函数访问非静态成员变量是因为类实例化生成为对象后,对象的非静态成员函数都拥有一个this指针,而实际上非静态成员函数对成员变量的访问都是通过这个this指针实现的(this就是对象指针)。而非静态成员函数并不包含this指针,所以只能通过类名形式如A::n访问成员变量,而支持该访问方式的只有静态成员变量。
⑤virtual:声明虚函数,用于实现多态,该关键字是基于声明的;
⑥friend:声明友元函数和友元类,该关键字也是基于声明的;
⑦volatile:被该关键字修饰的变量是指其值可能在编译器认识的范围外被修改,因此编译器不要对该变量进行的操作进行优化。可以与const同时修饰一个变量。
4、程序编辑、预编译、编译与链接
答:①编辑:也就是编写C/C++程序。
②预处理:相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有宏定义,没有条件编译指令,没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。
预处理注意事项:
③编译:将预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。
④链接:通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。链接是将各个编译单元中的变量和函数引用与定义进行绑定,保证程序中的变量和函数都有对应的实体,若被调用函数未定义,就在此过程中会发现。
5、引用库文件时使用双引号和尖括号的区别
答:使用#include” “表示引用用户库文件,在当前目录下查找,若没有就到标准库查找;
使用#include< >表示引用标准库文件,直接到到标准库查找;
所以,若引用标准库文件如stdio.h,用< >会比用" "查找快一些。
6、C/C++中的.h头文件中ifndef/define/endif
答:主要作用是防止重复引用,比如一个头文件定义如下:
#ifndef _HEAD_H_
#define _HEAD_H_
//主体代码
#endif
假如该头文件第一次被引用,_HEAD_H_没有被定义,所以就执行宏定义,直到#endif;
该头文件第二次被引用的时候,_HEAD_H_已经被定义,下面的语句就不会执行。
7、动态链接和静态链接
答:动态链接库:windows中是.dll,Linux中一般是 libxxx.a;
静态链接库:windows中是.lib,Linux中一般是 libxxx.so;
(1)静态链接与动态链接区别:
①静态函数库在链接时将整个函数库整合到中应用程序中并生成,所以程序生成文件较大;动态库相反,链接时不是所有数据都加载应用程序文件(只是加载导入库文件),所以生成文件较小。
②静态链接生成后的执行程序不需要外部的函数库支持,可以保持独立且可移植性好;动态库相反,需要库函数继续存在,可移植性不好。
③如果静态函数库改变了,那么你的程序必须重新编译链接;动态库相反,升级方便。
④静态库函数在程序链接阶段和应用程序一起完成链接生成;动态库函数要到程序运行时才链接生成。
⑤静态链接库不可以实现多程序共享,因为每个需要的程序都要将其编译生成到自己的目标文件中;而动态链接库则可以一个库文件被多个程序共享。
(2)动态链接情况下程序如何获得动态库函数?
动态链接时虽然不加载库函数本身,但是会加载一个导入库(lib包含目标库和导入库),导入库里面保存了动态库函数的函数名、参数等信息,程序运行时操作系统就通过这些信息找到函数库本身并加载。
(3)动态链接分为显式加载与隐式加载:
显式加载:在程序刚运行时就加载dll;
隐式加载:在程序运行需要库函数时再加载dll;
(4)
第二章、各种常规数据类型及相关运算
1、32位机和64位机的不同类型数据占用存储空间不同
答:注意:以下所区分的32位系统和64位系统都是针对Linux而言的:
一般注意32位系统中,short为2字节,int是4字节,float为4字节,long为4字节,double是8字节,指针占用4字节等就可以,64位除了指针占用8字节和long占用8字节,其他与32位相同。但注意,16位机器与32位有较大区别,如Int占用2字节,指针占用2字节等。
2、为什么const比宏定义好?
答:①可以指定类型,且有类型检查功能;②const量规定作用域规则,如在函数中定义常量,从而将其限制在函数内起作用;③可以将const用于更复杂的类型,比如数组和结构。
3、自动变量
答:(1)早在C++98标准中就存在了auto关键字,那时的auto用于声明变量为自动变量,自动变量意为拥有自动的生命期,这是多余的,因为就算不使用auto声明,变量依旧拥有自动的生命期:
-
int a =10 ; //拥有自动生命期
-
auto int b = 20 ;//拥有自动生命期
-
static int c = 30 ;//延长了生命期
(2)C++11中的自动变量不在是上述可有可无的作用,变为可以在声明变量的时候根据变量初始值的类型自动推导此变量匹配的类型,类似的关键字还有decltype。举个例子:
-
auto au_a = 10;//自动类型推断,au_a为int类型
-
cout << typeid(au_a).name() << endl;
这种用法就类似于C#或java中的var关键字。auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低。
(3)auto的作用:用于代替冗长复杂、变量使用范围专一的变量声明。
(4)auto的用法:
①代替比较长的类型名:
原始代码:
-
std::vector<std::string> vs;
-
for (std::vector<std::string>::iterator i = vs.begin(); i != vs.end(); i++)
-
{
-
//...
-
}
使用auto后的用法:
-
std::vector<std::string> vs;
-
for (auto i = vs.begin(); i != vs.end(); i++)
-
{
-
//..
-
}
for循环中的i将在编译时自动推导其类型,而不用我们显式去定义那长长的一串。
②在定义模板函数时,用于声明依赖模板参数的变量类型
-
template <typename _Tx,typename _Ty>
-
void Multiply(_Tx x, _Ty y)
-
{
-
auto v = x*y;
-
std::cout << v;
-
}
若不使用auto变量来声明v,那这个函数就难定义啦,不到编译的时候,谁知道x*y的真正类型是什么。
4、默认类型自动转换
答:在运算式中有多种数据类型,在没有强制类型转换情况下就需要编译器按照默认的自动转换,如下图:
其中,横向的箭头表示,在运算之前必须转换的;竖向的箭头表示运算过程中默认转换的顺序,也就是说,float类型的数据在运算之前,都转换为double类型的数据进行运算,同理,short和char类型的数据在运算之前,都是转换为int类型的数据进行运算。
这里补充一个知识点:浮点型数据在C++中不说明,默认是double。
例如,0.5默认就是double,所以fun(float c)调用时直接fun(0.5)就是不对的,会报参数不匹配错误。正确应该为:fun(0.5f)。
5、计算机中有符号数据存储形式
答:正数存储原码,负数存储为补码。
数据输出结果根据输出类型决定;
短类型数据转长类型数据,用短类型数据的符号位填充新增加的空白高位,例如:1000 0000(char)->1111 1111 1000 0000(short);
6、C++位操作符与逻辑运算符
答:(1)位操作如下图:
(2)逻辑运算符
A && B:A不成立,B就不判断了;
A || B:A成立,B就不判断了;
7、如何判断double变量是否为0?
答:首先,double变量由于精度截尾问题,是无法用==直接作比较的。所以一般就认定小于小数点后多少位开始是0。比如:
if( abs(f) <= 1e-15 )就是判断f是否为0的;
对于比较两个双精度a和b是否相等,相应的应该是:abs(a-b)<=1e-6;
对于float型数据比较,与double类似;
8、float和double数据类型的不同
答:(1)这里介绍下浮点型数据float和双精度double在32位机的存储方式(并不是直接将十进制转换为普通二进制就可以存储了),无论是float还是double,在内存中的存储主要分成三部分,分别是:
①符号位(Sign):0代表正数,1代表负数
②指数位(Exponent):用于存储科学计数法中的指数部分,并且采用移位存储方式
③尾数位(Mantissa):用于存储尾数部分
指数位用于表示该类型取值范围;尾数位用于反映有效数字。
(2)具体存储模式:
类型 符号位 阶码 尾数 长度
float 1 8 23 32
double 1 11 52 64
临时数 1 15 64 80
float的指数部分有8bit,由于指数也要分正负数(这个正负数不是float型数据的正负,而只是存储指数的正负)例如2^-2,其中-2就是负指数,所以得到对应的指数范围-128~128。 float的尾数位是23bit,对应7位十进制数;
double的指数部分有11bit,由于指数分正负,所以得到对应的指数范围-1024~1024,52个尾数位,对应十进制为15位;
(3)取值范围看指数,有效数字个数看尾数。
float取值范围:2^-128~2^128;有效数据位数:由于2^23=8388608为7位,所以理论上是7为有效数据,但实际编译器是8位(原因见下面);
double取值范围:2^-1024~2^1024;有效数据位数:由于2^52=4503599627370496为15位,所以有效数据位是15为有效数据;
(4)说了半天还只说了尾数可以反映有效数据位数,但并没说尾数是什么?下面解释:
例如9.125的表示成二进制就是:1001.001,进一步将其表示成二进制的科学计数方式为:1.001001*2^3 ;
实际上,在计算机中任何一个数都可以表示成1.xxxxxx*2^n 这样的形式。其中xxxxx就表示尾数部分,n表示指数部分。所以尾数部分就是待存储二进制数据的有效数据;
其中,因为最高位橙色的1这里,由于任何的一个数表示成这种形式时这里都是1,所以在存储时实际上并不保存这一位,这使得实际尾数位比存储的多一位,即float的23bit的尾数可以表示24bit(2^24十进制结果是8位,解释了3)的疑问),double中52bit的尾数可以表达53bit(2^53十进制依然是15位)。
(5)举个例子:
以下数字在表示为double(8字节的双精度浮点数)时存在舍入误差的有()。
A、2的平方根
B、10的30次方
C、0.1
D、0.5
E、100
答案:ABC
解析:A:2开平方根后结果的十进制都是无穷数,所以二进制形式一定是无穷的,那么根据二进制有效数据最多53个(或十进制15个),一定有舍弃部分;
B:2^90=(2^3)^30<10^30<2^100,那么10的30次方变二进制至少有90个有效数据位数,假定就是90个。然后将该十进制转化为二进制数(方法见下面(2)),10^30=5^30*2^30,故最后一个有效二进制数1在由低到高第31位处,而有效位数前面得到为90,所以有效位数为90-30=60>53,意味着要舍弃。
C:0.1变二进制形式是无穷位0.001100110011……,所以有舍弃;
D:二进制为0.1,无舍弃;
E:100二进制1100100,符合要求无舍弃;
9、几种常见有符号数的溢出问题
答:1)有符号数的取值范围都是负数部分绝对值比整数部分大1;,以char为例-128~127;
2)有符号数溢出后都是循环到数据范围的另一端,例如char型127+1结果就是-128,127+2结果是-127,……还有-128-1结果是127,-128-2结果是126,……以此类推;
下面分别解释上述1)、2):
①由于有符号数最高位为符号位,那么实际数据位就是n-1位。例如char型只有7个数据位,可以表示-127~127,但这样问题来了,数据位0分别和符号位组成了-0和+0,那么设计者就用+0表示0,-0表示-128,这就是-128的由来,也就是说用-128代替-0。
那为什么-128可以用-0表示呢?可能是因为-128二进制为1 1000 0000,但char型只有8位,所以要截断最高位,这样就和-0(1000 0000)一样了;
-128 二进制补码也为1 1000 0000,和原码相同,计算机中负数都是用补码存储的,因此-128在计算机存储中也应该是截断后的1000 0000,那么也可以看作-0的补码也不变化。
②这就可以解释为什么上述2)中char型127+1=-128和-128-1=127了?
127+1=
01111 1111
+0000 0001
=1000 0000
该值没有数据位溢出不用舍弃,结果刚好是-0,也就是-128的二进制值。
计算机二进制运算中没有减法或负数运算,只有加法运算,所有的负数或减法(减号当做负号,与减数组合成一个负数)都是先转化为相应补码,再进行二进制加法运算,那么-128-1就应该是(-128)补+(-1)补的结果:
-128-1=
1 1000 0000 (-128的补码与原码同)
+ 1111 1111
=11 0111 1111
高位溢出舍弃,用二进制表示结果为:0111 1111,即为127。
10、C++运算符优先级
答: 可如下记忆:
记住一个最高的:构造类型的元素或成员以及小括号;
记住一个最低的:逗号运算符;
剩余的是单目、双目、三目和赋值运算符(赋值运算符包括=、+=、-=、*=、/=、%=、……)。
注意若有逗号表达式的是:a=(表达式1,表达式2,……,表达式n)
结果为:a=表达式n,即逗号表达式的结果是最后一个表达式的值。但如果不加括号就是第一个表达式的值,因为赋值运算符优先级比逗号运算符高。
特别提出:指针运算符混合运算的优先级:由于单目运算符*和其他的一些单目运算符优先级同级,因此运算时根据从右至左结合方式。如下:
①一元运算符 * 和前置 ++ 具有相等的优先级别,但是在运算时它是从右向左顺序进行的,即在 *++p中,++ 应用于p而不是应用于*p,实现的是指针自增1,而非数据自增1;后置自增++的优先级实际上比*高,但使用右结合方式得到的结果与之一致,故为了统一表述,都可以直接使用右结合方式。如*p++,使用优先级是先++后*,右结合也是先++后*;
②指针运算符* 与取地址运算符&的优先级相同,按自右向左的方向结合;
11、为什么前缀自增自减运算符比后缀自增自减运算符效率高?
答:以自增运算为例,因为前缀运算符进行运算时是将值加1,然后返回结果即可;但后缀版本首先复制一个副本作为返回值,然后才是原值加1。因此,前缀版本的效率比后缀版本高。
12、C++中的四种类型转换方式(重要)
答:(1)常见的(type)类型转换是C语言中的类型转换方式,其有很多缺陷如:容易产生两个不和互相转换的类型被转换,从而引发错误等等。C++为了克服C中的缺陷,引入了四种更加安全的cast类型转换模式,如下:
①static_cast:最常用的类型转换符,正常状况下的类型转换,如把int转换为float,如:int i;float f;f=(float) i;或者用C++类型转换模式:f=static_cast<float>(i);
②const_cast:用于取出const属性,把const类型的指针变为非const类型的指针,如:const int *fun(int x,int y){}; int *ptr=const_cast<int *>(fun(2,3));
③dynamic_cast:通常在它被用于安全地沿着类的继承关系向下进行类型转换,但是被转换类必须是多态的,即必须含有虚函数。例如:dynamic_cast<T*> (new C);其中类C必须含有虚函数,否则转换就是错误的;
④reinterpret_cast::interpret是解释的意思,reinterpret即为重新解释,可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针。 如:int i; char *ptr="hello freind!"; i=reinterpret_cast<int>(ptr);
注:C++计算中的隐式类型转换是static_cast,转换中若表达式包含signed和unsigned int,signed会被转换为unsigned。例如:
int i = -1;
unsigned j = 1;
if(j > i)与if(i < j)
答案:上述两个判断都是false。
(2)这里补充“无符号数和有符号之间的转化关系”:
① 无符号数转换为有符号数:
看无符号数的最高位是否为1,如果不为1(即为0),则有符号数就直接等于无符号数;
如果无符号数的最高位为1,则将无符号数取补码,得到的数就是有符号数。
②有符号数转换为无符号数:
看有符号数的最高位是否为1,如果不为1(即为0),则无符号数就直接等于有符号数;
如果有符号数的最高位为1,则将有符号数取补码,得到的数就是无符号数。
具体解释可参见:http://blog.csdn.net/starryheavens/article/details/4617637
13、别名与宏的区别——typedef与#define的区别
答:类型别名与宏的三点区别,如typedef char *String_t; 和#define String_d char * 两句:
(1)前者是类型别名,要做类型检查,后者只是一个替换,不做类型检查;
(2)前者编译时处理,后者预编译时处理,即预编译期间替换掉宏;
(3)前者能保证定义的全都是char* 类型,String_d却不能,如String_t a,b;String_d x,y;其中a、b、x为char*类型,而y却是char类型,这点要注意(因为简单替换就是:char *x,y,y前面没有指针符就不是指针)。
14、C中的宏定义的开始作用位置
答:宏定义是在编译器预处理阶段中就进行替换了,替换成什么只与define和undefine文本中的定义位置有关系,与它们在哪个函数中无关。例如:
-
-
void foo();
-
void prin();
-
int main()
-
{
-
prin();
-
printf("%d ", a);
-
foo();
-
printf("%d ", a);
-
}
-
void foo()
-
{
-
-
-
}
-
void prin()
-
{
-
printf("%d ", a);
-
}
输出结果是:50 10 10
为什么?因为a重新定义只在print()前,估值对它有效。
第三章、复合类型——数组、字符串、string、指针与链表等
1、C++中字符串和普通数组初始化相关问题
答:C++有两种字符串表示模式:C风格的char []和string模式;其中C风格的char c[]初始化要注意的原则:char c[]数组末尾是‘\0’,没有就不是字符串,如char str[4]={'1','2','s','r'}就不是字符串。
数组初始化注意事项:
(1)使用{ }初始化只有定义是可以,以后就不能用了,如int c[4]={2,3,4,5}可以,但int d[4];d[4]={1,2,3,4}就错误;
(2)不可以将一个数组赋给另一个数组,如c=d是错误的,因为数组名是常量;
(3)C++11中用{ }初始化数组,可以省略=,如int c[4] {2,3,4,5}合法;
(4)使用{ }初始化禁止缩窄转换,如int p[3]={1,2,3.0}非法,因为浮点型的小数点后面数据被舍弃了,就是缩窄了;
下面介绍下C++的int型数组初始化:
二维数组初始化分为多种形式。注意,当只对部分元素赋初值时,未赋初值的元素自动取0值。例如:
①按行赋值
int a[ ][3]={{1,2,3},{4,5,6}};——相当于{{1,2,3},{4,5,6}}
int a[ ][3]={{1,2},{0}};——相当于{{1,2,0},{0,0,0}}
②连续赋值
int a[ ][3]={1,2,3,4,5,6};——相当于{{1,2,3},{4,5,6}}
int a[ ][3]={2};——相当于{{2,0,0}};
2、多维数组定义要点
答:多维数组定义中只有最靠近数组名的那个维数可以省略,其余不允许省略否则无法确定数组。例如:a[m][n]仅仅可以省略m,n必须定义。
3、int *p=new int(12)与int *p=new int[12]的区别
答:前者表示创建一个指针变量,其指向一个存储数字12的地址,后者表示创建一个长度为12的数组。
4、柔性数组
答:柔性数组在C++中只用在结构体中,且只能定义在结构体的末尾,如下:
struct Node
{
int size;
char data[0];
};
注:上述也可以char data[];
此时data只是表示一个数组符号,不占内存空间,就是一个偏移地址(一定注意,此时数组名data不是一个指针常量,是不占内存的)。
作用:长度为0的数组的主要用途是为了满足需要变长度的结构体,实现结构灵活使用,方便管理内存缓冲区,减少内存碎片化,一般都在结构体末尾。
在网络通信中的实际用途:由于考虑到数据的溢出,数据包中的data数组长度一般会设置得足够长足以容纳最大的数据,因此packet中的data数组很多情况下都没有填满数据,因此造成了浪费,而如果我们用变长柔性数组来进行封包的话,就不会浪费空间浪费网络流量。所以,柔性数组在通信中主要作用是,保证了维护数据包空间的连续性。
5、数组名的作用以及数组名前面添加取地址符的作用
答:1)数组名是数组首地址,是一个常量,不可以当作指针变量用,如:若str为数组名,str++就不合法,相当于常量自增。
再次注意:数组名是常量!常量!常量!常量就不可被赋值,若有char s[10];char *pt,则如下:
s="hello";//将常量赋给s,实质就是将常量首地址赋值给s;
s=pt;
都是错误的,s是数组名不可被赋值,任何形式的赋值都不可以。
2)同时,一维数组名当被直接使用时,是一个指向数组首地址的指针。
3)如果数组A是多维数组,那么数组名A是数组地址(是一个多级指针),而不是第一行地址(是一级指针),虽然它们的值都是首地址值。也就是说:以二维数组A[][]为例,A和A[0]是不同的,虽然地址值都是数组首元素地址,但是一个是二级指针,一个是一级指针。所以,只有*(A+i)与A[i]是等效的。
对于二维数组a[i][j],此时*(a+i)与a[i]是一个意思,当直接用a[i]时代表的是该数组第i行地址,所以多维数组a[][]的*(a[i]+j)或者*(*(a+i)+j)是与a[i][j]等效。
4)还有,数组名表示首地址,那么数组名前有取地址符是什么意思?例如:数组a[],a表示数组首元素地址,&a表示数组整体地址,&a+1就是该数组末尾后一个地址。
5)printf打印数组名:若char a[5]="abcd",printf("%d \n",a)则输出是数组首地址;若要输出字符需要printf("%C \n",a[i])一个个输出,如printf("%C \n",a[0])输出第一个元素。当然,printf("%s \n",a)是可以将数组作为字符串一起输出的。
6、sizeof对数组做运算的两种情况
答:分两种情况:
(1)直接计算:
-
char str[]="Hello";
-
cout<<sizeof(str)<<endl;
结果:6
(2) 通过形参传入:
-
void func(char str_arg[100])
-
{
-
cout<<sizeof(str_arg)<<endl;
-
}
-
func("test");
结果:4;
分析:(1)中是直接将数组名作为sizeof的操作数,所以就是计算数组本身空间大小(注意,不是数组长度),包括"\0",应该为6*1=6(单个char字符占一个字节),比较容易;(2)中的特点是数组作为函数的参数传递时,就是计算指针大小了。char str_arg[100]实际为 char *str_arg,传递的是指向数组首元素的指针,那么sizeof(char *str_arg)=4(32位Linux系统)。
补充注意:1)sizeof(malloc)等于4,因为malloc()返回的是一个指针。sizeof只对数组名的指向内容作内存计数运算;2)若有printf("%%%%\n"),只输出%%。因为每个输出前都有一个%表示输出。
7、sizeof以及sizeof与strlen的区别
答:(1)sizeof 操作符不能返回动态分派的数组或外部的数组尺寸(只用于返回静态数组的尺寸,且一般都在编译时就会运算结果)。具体如下:
sizeof 返回的值表示的含义如下(单位字节):
数组 —— 编译时分配的数组空间大小(不是数组长度);
指针 —— 存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为 4 );
类型 —— 该类型所占的空间大小;
对象 —— 对象的实际占用空间大小;
函数 —— 函数的返回类型所占的空间大小。函数的返回类型不能是 void 。
(2)sizeof 返回全部数组的尺寸;strlen时字符串长度计算函数,返回字符串实际长度,遇到'/0'则结束。遇到如:char str[20]="0123456789"; int a=strlen(str); //a=10; >>> strlen 计算字符串的长度,以结束符 0x00 为字符串结束。 int b=sizeof(str); //而b=20; >>> sizeof 计算的则是分配的数组 str[20] 所占的内存空间的大小,不受里面存储的内容改变。 上面是对静态数组处理的结果。
还有如:charc1[]={'a','b','\0','d','e'};strlen(c1)=2;因为strlen计算字符串的长度,会把c1当做字符串,遇到'/0'结束。
注:sizeof对于字符串会包括'\0',strlen不会包括'\0'。
(3)对于指针,sizeof会将其视为类型:
char str[] ="aBcDe";
char *str1 ="aBcDe";
cout << "str[]字符长度为: "<<sizeof(str)<<endl;//结果为6
cout<<"*str1字符长度为: "<<sizeof(str1)<<endl; //结果为4 系统32位
cout << "str字符长度为: " << sizeof(str) / sizeof(str[0]) << endl;//结果为6
cout << "str字符长度为: " << strlen(str)<< endl;//结果为5
(4)sizeof是运算符,strlen是函数;
8、C++中的字符串变量和字符串数组变量定义
答:(1)字符串变量定义:
1)用字符数组定义:char str[10];//静态定义,这种方式定义最普遍,适用于作为cin,scanf的输入接口;
2)用字符指针定义字符数组:char* str;//注意这种只是定义了一个指针,并未分配内存区域,无法直接向其写入数据,所以不适用于cin和scanf输入;
3)用字符指针定义字符数组并开辟内存区域:char *str=new char[10];//动态定义,这种是可以作为cin、scanf输入使用的,因为开辟了内存区域;
4)使用容器string也可以定义字符串,但是注意string字符串的长度获取一般使用size(),而char []主要是用<string.h>(属于C的库文件)中的strlen()函数。string str可直接用于cin或者scanf输入。
(2)字符串数组定义:
1)用char数组定义:char s[10][100];//静态定义
2)字符指针数组定义:char *s[10];//注意这种只是定义了指针变量,并未分配内存空间,无法向其直接写入数据,不可用于cin或scanf。使用前必须先赋值或分配内存。
3)用字符指针定义字符串数组并开辟内存区域:char *str[10]=new char[][100]; //动态定义,可用于cin或scanf输入;
4)使用string容器定义字符串数组:string str[10]; //静态定义,可用于cin或者scanf输入;
5)使用string容器定义字符串指针数组:string*
str; //未分配内存,不可用于cin或者scanf输入;
6)使用string容器定义字符串指针数组并分配内存区:string* str=new string[10]; //动态定义,可用于cin或者scanf输入;
9、C++11中的原始字符串
答:C++11中的原始字符串用R表示,定义为:R “xxx(raw string)xxx” ;
其中,原始字符串必须用括号()括起来,括号的前后可以加其他字符串,所加的字符串会被忽略,并且加的字符串必须在括号两边同时出现。
原始字符串可以直接把双引号"或者转义字符\n当做普通字符串输出,而标准的字符串则会将其当做特定功能符号如\n换行。
10、空串和空格串
答:长度为0 的串为空串,即为“” 。由多个空格字符构成的字符串称为空格串。
补充:空串是任何字符串的子串;
11、字符串比较
答:字符串char [](注意不是string,string是可以直接==比较的)比较需要使用函数strcm(str1,str2)函数,而不能直接使用“字符串1==字符串2”、“字符串1<字符串2”或者符串1>字符串2”;若有如下情况:
比较符号两边分别是一个地址或指针与字符串,那么比较的是字符串地址是否与被比较地址之间的关系,如:
char *p="hello";
return p=="hello";
返回值是1;
补充:如果是char[]类型的字符串,需要使用strcmp()函数;若是string串,直接使用if(s1>s2)之类比较即可。
12、(void *)ptr和(*(void**))ptr的结果是否相同?
答:相同。因为第一个就是把ptr强制转化为指向空类型的指针;
第二个(*(void**))ptr中的(void**)ptr是将str转化成指向void类型值的指针的指针,也就是二级指针。再在前面加上*就是取内容,那么内容也就是一个只想空类型的一级指针,所以强制转化后和第一个是一样的。
13、指针变量所占内存大小和指针加减有关的问题
答:指针相减的值:只有同类型的指针才可以相减,且“相减结果/单个变量类型内存大小”才是最终结果;
如若int *p=a[0],int *q=a[2],那么q-p=(q-p)/sizeof(int)=8/4=2是编译器默认的结果。
指针变量所占内存大小:32位操作系统是32位(4字节),64位操作系统是64位(8字节);
指针变量自增自减:自增自减中指针变量值的改变大小等于所指向的对象类型的内存大小。如:char* pt1,那么pt1++就是加1,若为int* pt2,那么pt2++就是增加4。指针运算问题都是以相应变量的类型大小作为基本单位的,例如int p[4]={0,0,0,0},p+1就是指p的地址基础上偏移4字节。同理,&p[0]+1也是一样的。除非(char*)&p[0]+1才是偏移一个字节,因为地址被强制转化为char*了。
补充:
(1)指针所指向的对象类型判断方法——去掉指针名及和它前面的*,剩下的就是指针类型。
(2)上面所说的地址地址++或--都是对于偏移地址offset而言的。
下面举例:
-
//验证32位系统中指针变量所占内存大小为4字节,并且指针变量自增(或自减)不一
-
//定是增加1,需根据所指向的对象类型决定
-
short strpt1[3]={1,2,3};
-
short* pt1=strpt1;
-
cout<<"char*指针变量所占内存大小:"<<sizeof(pt1)<<"\n";
-
cout<<"char*指针变量自增前结果:"<<pt1<<"\n";
-
pt1++;
-
cout<<"char*指针变量自增后结果:"<<pt1<<"\n";
-
//比较
-
int strpt2[3]={1,2,3};
-
int* pt2=strpt2;
-
cout<<"int*指针变量所占内存大小:"<<sizeof(pt2)<<"\n";
-
cout<<"int*指针变量自增前结果:"<<pt2<<"\n";
-
pt2++;
-
cout<<"int*指针变量自增后结果:"<<pt2<<"\n";
结果:
14、数组指针、指针数组、函数指针
答:区别如下:
(1)数组指针(也称行指针),定义 int (*p)[n];其中( )优先级高,首先说明p是一个指针,指向一个整型的一维数组(或二维数组的某一行),这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。
(2)指针数组,定义 int *p[n];其中[]优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]...p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 *p=a; 这里*p表示指针数组第一个元素的值,a的首地址的值。
若已定义: int a[]=[0,1,2,3,4,5,6,7,8,9],*p=a,i; 其中0≤i≤9,那么p[i]是否合法,若何法那么表示什么?
p[i]表示a[i]的指针地址,即p+i。
(3)函数指针,定义 int (*pf)(int *)为一个返回值为int,参数为int*的。
假如函数指针定义如下例:
void (*fp)(int);
则函数指针调用的两种格式:
1)(*fp)(9);
2)fp(9);
函数指针赋值的两种格式:
void fun(int s){……}
1)fp=fun;(函数名就是函数起始地址)
2)fp=&fun;(这种也可以)
注意,函数名后面一定不能有括号,否则就被认为是函数调用。
15、空指针问题
答:(1)空指针与野指针的区别:
空指针也就是通常指向为NULL的指针,常见的空指针一般指向 0 地址;
野指针是指向一个已删除或释放的对象或指向未申请访问受限内存区域的指针;
具体如下三种情况:
1)指针变量未初始化
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时未被初始化,它就是野指针。定义指针变量时要避免野指针,要么将指针设置为NULL,要么让它指向合法的内存;
2)指针释放后之后未置空
释放内存后指针要手动置为空,否则会产生野指针。有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是被释放区的“垃圾”内存,也就成为了野指针。释放后的指针应立即将指针置为NULL,防止产生“野指针”;
3)指针操作超越变量作用域
不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。
(2)空指针性质:
空指针不指向任何的对象或者函数,任何对象或者函数的地址都不可能是空指针;
malloc()申请内存空间失败的时候,人家返回的值为NULL,而不是任意的;
虽然空指针可能大多数指向0地址,但对0x0这个地址取值是非法的,因为系统规定是不会给你分配0地址的。
(3)空指针的printf输出
例如:char* str=null;
printf("%s\n",str);
在win系统输出结果:(null)
在Linux系统输出结果:Segmentation fault(出错)
注:若str是野指针,即未初始化,那么这个输出代码会造成程序崩溃。
16、静态链表
答:静态链表是用数组来描述的,也可以说是静态链表就是结构体数组。
数组中的结构体元素包括两部分:数据和指针,其中指针指向下一个元素在数组中所对应的下标。
1)使用数组对元素进行存储,在定义时大小已经确定,后期不可扩展容量;
2)静态链表的例子正好说明了数组相对链表除了可以随机访问外,还有另一个优势:相同的数据占用存储空间小(因为链表还要存储节点指针)。
第四章、复合类型——结构体、共用体、枚举等
1、C/C++中的结构体对齐原则
答:关于结构体内存对齐(在没有#pragma pack宏的情况下):
•原则1、数据成员对齐规则:结构(struct或联合union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员类型大小(注意是成员类型大小,而不是成员大小,如char a[10],应该取类型大小为1字节而不是数组大小10字节)的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)。
•原则2、结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部类型最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)
•原则3、收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员类型长度的整数倍,不足的要补齐(注意:补齐是补高位,就是填充0)。
举个例子:
struct stc
{
int a;
char b[9];
char c;
};
这个结构体的大小是16字节=4字节(int)+9字节(char*9)+1字节(char)+2字节(补全)
注意:
1)若结构体中含有静态成员,sizeof不包括静态和成员,因为静态成员存储在全局区(或静态区),sizeof计算的是栈空间大小(此结论亦适用于类);
2)如果编译器中提供了#pragma pack(n),上述对其模式就不适用了,例如设定变量以n字节对齐方式,则上述成员类型对齐宽度(应当也包括收尾对齐)应该选择成员类型宽度和n中较小者;
3)一般数据存储都是小端机器模式,即数据低位存储在低地址处。
补充:
(1)位域:所谓"位域"是把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。结构体中存在位域时,对于位域变量大小有如下规定:
1) 如果相邻位域字段的类型相同,且紧邻的位域位宽之和小于给定类型的sizeof大小,则后面的字
段将紧邻前一个字段存储;
2) 如果相邻位域字段的类型相同,但其位宽之和大于给定类型的sizeof大小,则后面的字
段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方
式,Dev-C++采取压缩方式;
4) 如果位域字段之间穿插着非位域字段,则不进行压缩;
5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。
(2) sizeof(类):类的大小计算:类的存储大小sizeof运算也可以当做结构体来计算,注意函数声明不占内存。
但是要注意继承问题:比如派生类继承基类,那么:
派生类大小=基类成员(不包括静态成员)+自己本身
注意:
1)类的静态成员由于存放在全局区域,为所有类共享,sizeof运算不包括静态成员;
2)虚函数在类中有一个函数指针指向虚表,例如,对于一个类对象如obj,其中指针运算*obj,有如下两种情况:
①类中有虚函数,那么虚函数表的指针将放在所有类对象成员最前面,那么*obj指向虚函数表指针。在32位系统中,指针是4个字节,那么对象内存的前4个字节都是存储虚函数表指针,第5个字节才开始存放类成员;
②若无虚函数,则*obj直接指向按顺序类中的第一个成员。
③所以若有虚函数声明的类,sizeof运算会将虚函数表指针大小算在内(32bit为4字节)。无论类中有几个虚函数,sizeof类都等于sizeof(数据成员)的和+sizeof(一个V表指针,为4);
④对于上述③中所说的只是在单继承或非继承时成立,派生类多继承时每继承一个含有虚函数的基类,其就增加一个虚函数表指针。若其本身还有虚函数,则该虚函数地址存放在派生类的第一个虚函数表中;
3)空类,或者类中只有函数声明(虚函数除外),没有成员变量的类,其大小sizeof(空类)=1。注意,空类求sizeof不考虑对齐问题;
4)对于子类,它的sizeof是它父类成员(无论成员是public或private),再加上它自己的成员,对齐后的sizeof;
5)对于子类和父类中都有虚函数的情况,子类的sizeof是它父类成员(无论成员是public或private,但不包括父类虚表指针),再加上它自己的成员,对齐后的sizeof,再加4(子类自己的虚表指针);
6)若既有虚函数又有虚继承,那么子类sizeof=基类sizeof(包括基类虚指针)+子类sizeof(包括子类虚指针)+子类指向基类的指针(32bit为4字节)(这个要注意)
更多sizeof运算参考:http://blog.csdn.net/u014186096/article/details/48290013
(3)C/C++中的联合体(共用体):联合体是类似结构体的,但联合体中的成员是共用内存的,也就是联合体中的成员共用同一段内存(这也是联合体存在的作用,用于节省内存),其内存大小根据成员类型最大长度的对齐原则确定,而且,联合体中每一时刻只有当前的一个成员有效,即需要用哪个成员,就对共用内存重新写入该成员的数据,不重写读出来的就还是前一个成员使用的数据;
注意:有人说联合体的变量不能在定义时初始化,这是错误的。联合体变量可以在定义时初始化,但是初始化的值必须是第一个成员变量,而且要用{}括起来。如下:
union Test
{ char m[6];
int a;
float b;
};
Test test = {1};
因为最大长度的类型是int或者float,都为4,而m[6]占用6字节内存。所以采用补齐原则,联合体应为4*2=8字节内存,即sizeof(test)=8;
2、共用体union用于判断机器是小端存储还是大端存储
答:由于union只存储一个成员,若一个union有一个int变量和一个插入变量,那么若前一个int变量被赋值后,此时union存储的就是该int变量。若此时读取后一个char变量,由于char并没有被重写,所以读取的还是int变量的低8位。根据读取的int低八位数字就可以判断大端或小端存储了。
代码如下:
-
bool IsLittleEndian(){
-
union{
-
int a;
-
char b;
-
}u;
-
-
int k=15;//要在char范围内
-
u.a=k;
-
if((int)u.b==k)
-
return true;
-
return false;
-
}
3、C Plus的struct及其与class的区别
答:(1)C++的结构体与C的结构体区别:
C++中的struct对C中的struct进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了,它已经获取了太多的功能:
①C的struct不可以包含成员函数,C++的struct能包含成员函数;
②C的struct不可以继承,C++的struct能继承;
③C的struct不可以实现多态,C++的struct能实现多态;
(2)C++的struct和class的区别:
最本质的一个区别:成员默认属性和默认继承权限的不同;
①若不指明,struct成员的默认属性是public的,class成员的默认属性是private的;
②若不指明,struct成员的默认继承权限是public的,class成员的默认继承权限是private的;c和c++中struct的主要区别是c中的struct不可以含有成员函数,而c++中的struct可以。
补充:c++中struct和class的主要区别在于默认的存取权限不同,struct默认为public,而class默认为private。
4、C++中枚举变量的定义
答:举例:
enum en1{
x1,
x2,
x3=10,
x4,
x5,
} emx1;
其中,emx1就是一个枚举变量,也可en1 emx1来定义枚举变量。
枚举变量不初始化,若该变量是全局变量,则系统自动初始化为0;若为局部变量,则为随机值。注:上述枚举缺省赋值,则系统自动根据已有元素值依次赋值(首位缺省自动赋值为0),所以x1~x5依次为:0,1,10,11,12。
第五章、静态变量、常量、全局变量与局部变量等
1、C++中类的静态成员static和常量(准确应该是“只读”)成员const
答:(1)静态成员static
特性:在C++中,静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共享。
注意事项:
①、静态成员在C++中既可以被类名引用,也可以被对象引用,这与C#不太一样。但注意不能通过类名来调用类的非静态成员函数;
②、静态成员函数不可以引用非静态成员变量,因为静态成员函数属于整个类,在类实例化对象之前就已经分配空间了,而类的非静态成员必须在类实例化对象后才有内存空间。但反之,类的非静态成员函数可以调用用静态成员函数;
③、类的静态成员变量必须先初始化再使用,并且静态成员必须在类体外进行初始化(因为它是多个对象共享的),不可以在类中声明时就初始化,格式如:int Info::mm=9;
④静态成员变量和函数在类声明外定义实现时都不能再加static关键字。
举例:
-
class Info{
-
public:
-
static int mm;
-
static const char nn=97;
-
Info()
-
{
-
}
-
};
-
int Info::mm=9;
-
int _tmain(int argc, _TCHAR* argv[])
-
{
-
Info info; //调试可知,在类对象声明时就会调用构造函数
-
//静态数据成员初始化及调用
-
cout<<Info::mm<<"\n";
-
cout<<info.nn<<"\n";
-
getchar();//等待键盘Enter键关闭
-
return 0;
-
}
结果:
可见:static const可以在声明时赋值,其他的都在外面用::引用赋值。且静态成员支持类::引用和对象引用两种模式。
补充:类的静态成员变量是被所有类对象共享的;
类成员函数中定义的静态变量是被所有类对象中对应的那个成员函数所共享的(对其他成员函数无效)。
(2)常量const
在类中,我们可能不希望某个值在程序中的任何地方被修改,就像Math类中的PI那样,那么,我们可以使用成员常量来实现。声明方式如:const 类型 常量名;
-
Info():ee(111)
-
{
-
//mm=8;
-
cout<<"我是构造函数";
-
}
即将const int 变量ee初始化为111。若有多个要初始化,在ee()后面用逗号分隔。注意:C++中单纯的const常量不可以直接在声明时初始化(这是与C、C#不同的),需要在构造函数初始化表中初始化,如下:
提醒:静态变量不可以使用初始化列表。
(3)static const类型的类成员
也就是(1)中所提到的,c++中static const类型成员应该是static和const(只读)合在一起的解释,那么应该有如下原则:
①、有const修饰表示初始化值不可修改;
②、有static修饰表示是对象共有的,只是由于它是在类成员函数中定义的,那么其作用范围只在对应的对象成员函数中,在对象其他成员函数中是无效的。
其初始化有如下规定:
①、static const和const static是同样的;
②、static const由于有static修饰,所以可以在类体外进行初始化,方式同单纯的static变量;但注意,虽然有const修饰,但不可以像const那样用初始化列表初始化;
③、但其又和单纯的static有所不同,static const int型在声明时初始化在C++中也是合法的,如static const int nn=97;
2、全局变量的声明定义与引用
答:全局变量声明与定义:
(1)在库文件中用extern声明全局变量(不包括定义)
举个例子,在test1.h中有下列声明:
-
-
-
extern char g_str[]; // 声明全局变量g_str
-
void fun1();
-
-
在test1.cpp中
-
-
char g_str[] = "123456"; // 定义全局变量g_str
-
void fun1()
-
{
-
cout << g_str << endl;
-
}
以上,现在库文件用extern声明全局变量,那么全局变量的引用方式有两种:
①文件中引用库文件并定义(若全局变量已经在其他引用程序中定义,此处不需再定义);
②文件中使用extern引用并定义(同①,若已定义不需再定义);
(2)在库文件中用extern声明并定义全局变量(包括定义)
把全局变量的声明和定义放在一起,这样可以防止忘记了定义,如把上面test1.h改为:extern char g_str[] = "123456";
那么全局变量的引用只有一种方式:extern引用(注:若还用引用库文件会造成全局变量定义语句执行两次,引发错误);
3、类成员函数中的静态变量作用域
答:静态变量定义在类成员函数中的作用周期是和该成员函数一样的,成员函数被销毁该静态变量存储内存就会释放。所以有如下结果:
-
void Info::showtest()
-
{
-
static int num=0;
-
num++;
-
cout<<num<<" ";
-
}
-
Info info;
-
info.showtest();
-
info.showtest();
-
info.showtest();
运行:
可知:并不是每次调用showtest()都会初始化num=0,由于它是静态的,所以只有第一次初始化有效。编译到成员函数中变量定义处时进行初始化,以后就不在进行初始化。其有全局变量的作用周期,但只有局部变量的作用域。
4、const在指针符*前和后的区别
答:int const *p是常量指针(const of point),int * const p是指针常量(point of const),名称与指针符*与const的位置关系是对应的;const在*号前表示指针所指向的数据是常量,不可更改;const在*号后表示指针本身是常量,不可更改。下面有一个例子:
char a[] = "hello";
const char* p1 = a;
注意:int const与const int一样。
5、常量折叠
-
-
using namespace std;
-
int main(void)
-
{
-
const int a = 10;
-
int * p = (int *)(&a);
-
*p = 20;
-
cout<<"a = "<<a<<", *p = "<<*p<<endl;
-
return 0;
-
}
第六章、逻辑表达式与关系语句等
1、C++中的switch语句
答:(1)不能作为switch语句参数的有字符串和浮点型(float、double)
① char、short、int、long、bool 基本类型都可以用于switch语句。
② float、double都不能用于switch语句(因为一旦在比较中出现精度不一致问题就没法继续了)。
③ enum类型,即枚举类型可以用于switch语句。
④ 所有类型的对象都不能用于switch语句。
⑤ 字符串也不能用于switch语句(switch是用"="进行比较,而char[]没有"="的概念,只有strcmp)。
(2)switch在编程语言中被划为“循环语句”
但是注意,switch循环中不可以使用continue,只可以使用break;
continue只可以在for/while/do while中使用;
2、C++中的单独{ }作用
答:在C++中单独{ }括起来的叫做程序块,其对程序执行顺序并无改变,只是限定其中定义的变量,即其中定义的变量出了该花括号就无效了。
时刻谨记:局部变量只要出了{ }就失效。
3、标签label
答:如:
-
int main(void) {
-
http://www.taobao.com
-
cout << "welcome to taobao" << endl;
-
}
该代码段是可以编译并执行通过的,因为编译器会把http:当做label,即goto语句的目标。类似:
step1:a=fun();
goto step1;
总结:标签常用在goto语句中;
4、赋值表达式作为判断语句的问题(特殊问题)
答:赋值表达式作为判断语句使用时,若赋值为非零时表达式为真,赋值为0时为假。例如:
-
for(i=0,j=-1;j=0;i++,j++)
-
{
-
k++;
-
}
结果是:循环一次都不会执行。
第七章、函数相关
1、函数重载
答:函数重载目的:使用方便,规范编程代码,提高可靠性。(注:并不可以节省空间)
其3个判断依据分别是:参数类型、参数个数、const;
2、函数默认的传参顺序为从右到左
答:函数的默认参数传递顺序是从右到左。
例如:
①int i = 3; printf("%d %d", ++i, ++i),运行输出为:5 4;
②对于printf函数,变量参数多于输出格式符:
-
int a=12,b=2,c=89;
-
printf("%d%d",a,b,c);
结果为:2 89。
3、函数中不可被修改的参数确定原则
答:a) 始终用const限制所有指向只输入参数的指针和引用;
b) 优先通过值传递来取得原始类型int、float、char等和开销比较低的值的对象;
c) 优先按const的引用取得其他用户定义类型的输入;
d) 如果函数需要其参数的副本,则可以考虑通过值传递代替通过引用传递。这在概念上等同于通过const引用传递加上一次复制,能
够帮助编译器更好的优化掉临时变量。
4、C++函数参数取默认值相关问题
答:(1)作用:C++中有时多次调用同一函数时用同样的实参,C++提供简单的处理办法:给形参一个默认值,这样形参就不必一定要从实参取值了。如有一函数声明:float area(float r=6.5);指定r的默认值为6.5,如果在调用此函数时,确认r的值为6.5,则可以不必给出实参的值。
如:area( ); //相当于area(6.5);
(2)规定:指定某个参数的默认值,那么必须也指定其右端的所有参数默认值,否则调用函数省略有默认值的实参时,编译器匹配参数会出错。
如:若有float volume(float h,float r=12.5); //只对形参r指定默认值12.5函数调用可以采用以下形式:
-
volume(45.6); //相当于volume(45.6,12.5)
-
volume(34.2,10.4) //h的值为34.2,r的值为10.4
但是如下可能就有问题:
-
void f1(float a,int b=0,int c,char d); //不正确
-
void f2(float a,int c,int b=0, char d='a'); //正确
对于f1,若调用时是f1(1.2, 2, 3),那么编译器不知道参数2到底是赋给b还是c的,所以报错。
5、main主函数的返回值的作用
答:main主函数不能被调用,为什么还有返回值?
因为C语言实现都是通过函数mian的返回值来告诉操作系统函数的执行是否成功,0表示程序执行成功,返回非0表示程序执行失败,具体值表示某种
具体的出错信息.还有虽然别的函数不能调用main函数,但系统可以调用main的。
6、带参数的主函数相关问题
答:一般情况下,我们在写程序的时候,往往忽略了主函数的参数,例如:
int main(){ }
在命令行下,输入程序的名称就可以运行程序了。实际上,我们还可以通过输入程序名和相关的参数来为程序的运行提供更多的消息。参数紧跟在程序名后面,参数之间用空格分开。这些参数被称为:command-line arguments(命令行参数),也往往被称为程序的argument list(参数表)。main函数通过两个参数获取输入参数表信息,分别是argv和argc。第一个参数是一个整型的变量,它记录了用户输入的参数的个数(参数个数包括程序名),第二个参数argc是一个char型的指针数组,它的成员记录了指向各参数的指针,并且argc[0]是程序名,argc[1]是第一个参数。例如:
-
-
int main(int argv, char *argc[])
-
{
-
printf("/nthe name of the program is %s /n", argc[0]);
-
printf(" the program has %d argument! /n", argv - 1);
-
if(argv > 1)
-
{
-
int i;
-
printf("the arguments are:");
-
for(i=1; i<argv; i++)
-
{
-
printf("%s/t",argc[i]);
-
}
-
}
-
return 0;
-
}
若该程序名为mytest,那么输入:
mytest aa bb cc dd e
则输出结果是:
the name of the program D:/WINYES/TCPP30E/OUTPUT/MYTEST.EXT
the program has 5 argument!
The arguments are: aa bb cc dd e
由上可知:argv=6,argc[i]是第i个参数的首地址。
7、C++中的引用变量如何定义与使用?
答:(1)C++引用变量定义举例:
int a;
int& b=a;
上述就是引用变量定义方式,引用变量没什么特别的意义,仅仅是变量的一个别名,如上b就是a的一个别名。
①注意不要把int&看做地址符;
②还有就是除了把引用变量当做函数返回值或参数外,其他情况声明引用变量就需要直接在当前初始化,以告诉编译器该引用属于哪个变量的别名。
(2)一般作为函数参数使用,但引用变量使用中要注意的地方:
1)不要将局部引用变量作为返回值,否则编译警告且执行出错。因为引用变量返回是返回本身,不是像普通局部变量一样拷贝返回,所以一旦函数结束,局部应用变量就会自动销毁,从而造成返回出错。
2)引用作函数的形参,函数将使用原始数据而不是其拷贝。
3)常引用的作用在于提高程序效率(没有拷贝、进栈出栈等操作),同时还使得函数不可以更改引用变量值。
8、函数体内对形参进行修改会对实参产生改变吗?
答:分两种情况:
1)若传的是地址或引用,那么形参,实参指向同一内容,修改形参会影响实参;
2)若传的是值,因为实参值是拷贝到形参的,所以修改形参不会影响实参。
9、函数返回值问题——局部指针和局部数组地址是否都不可以作为返回值
答:常说的局部变量的地址不可以作为返回值,因为函数结束就释放了,但有例外,即局部指针指向字符串时可以作为返回值,见如下解析:
(1)字符串赋值给指针时字符串是常量,赋值给数组时字符串是变量
对上述所说的字符串因初始化对象不同而成为变量或常量,补充一个例子:
-
char*p1 = ”123”, *p2 = ”ABC”, str[50] = “xyz”;
-
strcpy(str + 2, strcat(p1, p2));
-
printr(“%s\n”, str);
该程序回编译出错。
因为*p1="123"是常量,长度不可变化,strcat()函数显然不可用。
(2)根据上述可知:局部指针变量指向字符串时可以作为函数返回值,而局部数组变量地址则一定不可以作为返回值;
以局部数组和局部指针变量为例:当局部指针变量的初始化值是字符串时如char* p="hello",可以作为返回值,因为指针所指的字符串是常量,存储在常量区,不随函数结束而销毁;
但当返回值是局部数组时,即使初始化值和上述一样是字符串,例如char s[]="12345",但该字符串也只是变量,数组内容也就也就是局部变量。简单说:由于初始化数组的字符串是变量,用于初始化指针的字符串是常量。局部数组变量在函数调用结束之后,也就被操作系统销毁了,即回收了他的内存空间,它里面的东西随时都有可能被覆盖。虽然此时我们获得了指向这一块内存的指针,但指向的内容已改变。
例如下面的例子就是返回值错误:
-
char* test2()
-
{
-
char p[] = "hello world";
-
return p;
-
}
若其中p改为char *p="hello world",那么返回值不会出错。因为这里的"hello world"是作为字符串常量存在的,存储在常量区域,函数结束也不会销毁。而前者数组中"hello world"是作为变量存在的,其每个字符都是局部变量,存储栈中,函数结束就会销毁,所以最后返回的p所指向的内容并不是"hello world"。
结论:在子函数中,用于初始化指针变量的字符串是常量,该字符串本身不可被修改,存储在常量区,生命周期至程序结束;用于初始化数组的字符串是局部变量,该字符串本身可以被修改,存储在栈中,生命周期随子函数结束。
(2)进一步得出:除局部静态常量外的其他局部变量(包括局部常量)的地址都不可以作为指针类型返回值;
注意:上述仅仅是返回值是指针类型时,若返回不是指针类型,返回数据会直接拷贝,就不会存在本小节所面临的问题;
关于局部变量地址做返回值有个例外,就是静态局部变量,静态局部变量地址可做指针返回值。例如:
-
-
-
using namespace std;
-
-
char* getStr1()
-
{
-
char ch='a';
-
printf("%p\n", &ch); //输出地址
-
return &ch; //报错,局部变量的地址/指针不可作为返回值
-
}
-
const char* getStr2()
-
{
-
const char ch='a';
-
printf("%p\n", &ch);
-
return &ch; //可返回地址,但主函数无输出或输出值不确定
-
}
-
char* getStr3()
-
{
-
static char ch='a';
-
printf("%p\n", &ch);
-
return &ch; //正确
-
}
-
-
int main(void)
-
{
-
char* p=getStr3(); //仅仅静态局部变量的地址或指针可作为返回值
-
printf(" ");
-
printf("%c\n", *p);
-
return 0;
-
}
10、回调函数
答:回调函数就是一个通过函数指针调用的函数,当该函数的指针(地址)作为参数传递给被调函数时才能称作回调函数。回调函数也可以象普通函数一样被程序调用;
-
void perfect(int n)
-
{
-
//******//
-
}
-
void myCallback(void (*perfect)(int ),int n)
-
{
-
perfect(n);
-
}
-
int main()
-
{
-
int n=9;
-
myCallback(perfect,n);
-
return 0;
-
}
在main中调用myCallback时,perfect作为函数指针参数传入被调用的函数,此时perfect就是回调函数。
由上可知pa->out()和pa->out(3)调用都是函数A::out(int i),
由上可知pb->out()和pb->out(4)调用都是函数B::out(int i),
由上可知p1b->out()和p1b->out(5)调用都是函数B::out(int i),
p1b->out()时,p1b的静态类型是B*,它的缺省参数是2;调用的也是B::out(int i);
作用:回调函数的作用在于保证调用函数内部实现的灵活性。
11、内联函数
答:内联函数:即函数定义(注意是定义不是声明)位于类声明中的函数,且内联函数比较小如:
-
class stock
-
{
-
private:
-
char name[];
-
int score;
-
void set_tot(){cout<<”my name is XC”;} //set_tot()就是内联函数
-
public:
-
void acquire();
-
void buy();
-
}
当然,除了这种内联函数外,还可以像传统的一样在类外使用关键字inline定义内联函数。
作用:避免普通的函数调用所产生的额外时间消耗,提高函数调用效率。就像宏一样,内联函数的代码是放到了符号表中的,使用时直接复制过来就好了,不用去做调用中耗时的内部处理过程(即:PC进栈出栈过程)。
补充:inline函数并不一定就被编译器视为内联函数,编译器会就情况自动确定;
内联函数在编译阶段直接替换函数体,类似宏在预编译阶段替换一样。但内联函数和宏有最大不同点:
内联函数在编译阶段会做类型参数检查,这也是其相对于宏最大的优势。
12、函数调用约定方式
答:函数调用约定(calling convention)不仅决定了发生函数调用时函数参数的入栈顺序,还决定了是由调用者函数还是被调用函数负责清除栈中的参数,还原堆栈。函数调用约定有很多方式,除了常见的__cdecl,__fastcall和__stdcall之外,C++的编译器还支持thiscall方式,不少C/C++编译器还支持naked call方式。
(1)__cdecl调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printf和windows的API wsprintf就是__cdecl调用方
式。
13、C语言中的strcpy和strcat函数
答:(1)strcpy(dest,src):C语言标准库函数strcpy,把从src地址开始且含有'\0'结束符的字符串复制到以dest为开始的地址空间,返回指向dest的指针。注意,dest一定要先分配足够大小地址空间,否则复制操作会造成程序崩溃。如下就有问题:
-
int main()
-
{
-
char a;
-
char *str=&a;
-
strcpy(str,"hello"); //str没有分配合适大小的地址空间,str只是一个字符的地址空间指针
-
printf(str);
-
return 0;
-
}
(2)strcat(dest,src):把src所指字符串添加到dest结尾处(覆盖dest结尾处的'\0')并添加'\0',返回指向dest的指针。
14、C++中c_str()函数
答:c_str()返回值是const char*,返回一个指向正规C字符串的指针;
-
string a="hello world";
-
string b=a;
-
if(a.c_str()==b.c_str())
-
{
-
cout<<"true"<<endl;
-
}
-
else
-
cout<<"false"<<endl;
上述代码中,b=a是拷贝赋值操作,即b开辟新内存,将a所指向的字符串常量拷贝到其中,那么b.c_str()的的返回指针自然与a.c_str()不同,所以打印false。若b="hello world",那么打印true,因为"hello world"是字符串常量,保存在全局区,这样赋值是直接将字符串常量地址赋给b。
15、STL容器名如string作函数参数是属于值传递
答:STL容器作为函数参数是值传递而不是地址传递,即使实参是容器名,但其与数组名作实参是完全不同的。
要实现函数中对实参的修改就是对原容器修改,一般使用引用传递。两种如下:
STL容器值传递:会将创建一个新的容器并将原容器数据拷贝过来
STL容器引用传递:传递原容器的别名,函数中操作的就是原容器本身
第八章、C++内存模型
1、C++中各存储区域划分、内存分配时间、内存释放模式
答:(1)栈区(stack)—— 由编译器自动分配释放 ,存放为运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
(2)堆区(heap)——一般由程序员分配释放, new, malloc之类的,若程序员不释放,程序结束时可能由OS回收 (注意:堆不可以静态分配,静态分配都是在编译阶段分配的)。
(3)全局区(或者叫静态区)(static)— 存放全局变量、静态数据、常量。程序结束后由系统释放。
(4)文字常量区 — 常量字符串就是放在这里的,如string str="Hello!"中的"Hello!"就存放在文字常量区。程序结束后由系统释放。
(5)程序代码区 — 存放函数体(类成员函数和全局函数)的二进制代码。
如下:
-
int a=0;
-
class someClass{
-
int b;
-
static int c;
-
};
-
int main(){
-
int d=0;
-
someClass *p=new someClass();
-
return 0;
-
}
a、c在全局区;b、p在堆区(由于成员变量会成为对象的成员,所以b在堆区);d在栈区。
内存分配时间:
①编译时不分配内存
编译时是不分配内存的。此时只是根据声明时的类型进行占位,到以后程序执行时分配内存才会正确。所以声明是给编译器看的,聪明的编译器能根据声明帮你识别错误;
②运行时必分配内存
运行时程序是必须调到“内存”的。因为CPU(其中有多个寄存器)只与内存打交道的。程序在进入实际内存之前要首先分配物理内存。注意,涉及到内存分配的都是在运行阶段分配才有意义。
内存释放的两种模式:两个关键字delete和free释放内存,new和delete搭配、malloc和free搭配;
2、C++内存模型
答:C++内存模型组成有三部分:自由区、静态区、动态区;
根据c/c++对象生命周期不同,c/c++的内存模型有三种不同的内存区域,即:自由存储区,动态区、静态区。
自由存储区:局部非静态变量的存储区域,即平常所说的栈;
动态区: 用new ,malloc分配的内存,即平常所说的堆;
静态区:全局变量,静态变量,字符串常量存在的位置;
注:代码虽然占内存,但不属于c/c++内存模型的一部分;
更多参考:http://blog.csdn.net/xiongchao99/article/details/74524807#t3
3、realloc,malloc,calloc的区别
答:三个函数的申明分别是:
void* realloc(void* ptr, unsigned newsize);
void* malloc(unsigned size);
void* calloc(size_t numElements, size_t sizeOfElement);
都在stdlib.h函数库内,它们的返回值都是请求系统分配的地址,如果请求失败就返回NULL ;
malloc用于申请一段新的地址,参数size为需要内存空间的长度,如:
char* p;
p=(char*)malloc(20);
calloc与malloc相似,参数sizeOfElement为申请地址的单位元素长度,numElements为元素个数,如:
char* p;
p=(char*)calloc(20,sizeof(char));
这个例子与上一个效果相同
realloc是给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,newsize是重新申请的地址长度 ,
如:
char* p;
p=(char*)malloc(sizeof(char)*20);
p=(char*)realloc(p,sizeof(char)*40);
注意,这里的空间长度都是以字节为单位。
C语言的标准内存分配函数:malloc,calloc,realloc,free等;
C++中为new/delete函数。
4、C++存储方案
答:C++有三种,C++11有四种,这些方案的区别就在于数据保留在内存中的时间。
自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。C++有两种存储持续性为自动的变量;
静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。C++有3种存储持续性为静态的变量;
线程存储持续性(C++11):当前,多核处理器很常见,这些CPU可同时处理多个执行任务。这让程序能够将计算放在可并行处理的不同线程中。如果变量是使用关键字thread_local声明的,则其生命周期与所属的线程一样长。本书不探讨并行编程;
动态存储持续性:用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储(free store)或堆(heap)。
第九章、类与对象
1、C++中声明对象时使用new和不使用new的区别?
答:简而言之:C++中类对象声明中使用new一般是定义类指针(注意C++的new只可用于类指针,不可用于类对象),这种方式创建的类指针需要在使用结束时进行delete释放内存,否则会造成内存泄露;而不使用new的对象声明是由系统自动创建并释放内存的,不需要我们手动释放。具体区别可见如下代码:
-
class Info{
-
private:
-
string name;
-
int age;
-
public:
-
Info()
-
{
-
cout<<"我是构造函数";
-
}
-
void show(string);
-
};
-
void Info::show(string name)
-
{
-
cout<<name<<"\n\n";
-
}
-
int _tmain(int argc, _TCHAR* argv[])
-
{
-
cout<<"声明info对象"<<"\n";
-
Info info; //调试可知,在类对象声明时就会调用构造函数
-
info.show("info");
-
//注意:以下这种做法在C++中是错误的,会提示: 无法从“Info *”转换为“Info”,
-
//这种方式只适合C#和Java中
-
//Info infor=new Info();
-
cout<<"声明infom类通用指针"<<"\n";
-
Info* infom; //此处仅仅相当于一个通用的类指针,不会调用构造函数
-
cout<<"infom指针初始化"<<"\n";
-
infom=new Info();
-
infom->show("infom");
-
cout<<"infoma指针声明并创建new"<<"\n";
-
Info* infoma=new Info(); //类指针创建new时会调用构造函数
-
infoma->show("infoma");
-
getchar();//等待键盘Enter键关闭
-
delete infom;
-
delete infoma;
-
return 0;
-
}
运行结果如下图:
另外由上述结果可知:声明对象Info info和new Info()会调用构造函数,但Info* infom不会调用构造函数。但对于A* p=new B之类的对象指针创建,会调用B的构造函数(若B是A的子类,根据继承关系的对象构造,会先调用A的构造函数,再调用B的构造函数)。
2、malloc与new在创建对象内存时的主要区别
答:new 不止是分配内存,而且会调用类的构造函数,同理delete会调用类的析构函数,而malloc则只分配内存,不会调用类的构造函数进行初始化类成员的工作,同样free也不会调用析构函数。
malloc函数的用法:void *malloc(int size);
注意:malloc只可以用来分配内存(分配的是虚拟内存,不是物理内存),还有void*并不表示返回空,这里表示需要程序员自行指定返回指针类型,是强制返回任何类型的指针,比如:int *p=(int *)malloc(size)。
更详细区别参见另一博文:http://blog.csdn.net/xiongchao99/article/details/74524807#t18
3、多态种类
答:多态分为两类:通用多态和特定多态;
(1)通用多态:参数多态、包含多态;
①参数多态:包括函数模板和类模板
②包含多态:virtual虚函数
(2)特定多态:重载多态、强制多态
①重载多态:重载多态是指函数名相同,但函数的参数个数或者类型不同的函数构成多态
②强制多态:强制类型转换
4、纯虚函数
答:定义:virtual void funtion1()=0;
有纯虚函数的类是抽象类,不可以生成对象,抽象类只可以派生,由他派生的类的纯虚函数没有重写的话派生类还是一个抽象类;
5、C++多态性(包含多态)以及虚函数应用
答:C++的多态性:多态性表示使用父类的指针指向子类的对象,这样就可以使用父类类型的对象指针调用子类的成员函数,其实
就是父类虚函数的应用。
虚函数和纯虚函数的主要作用:实现多态性,即:虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。;
(1)虚函数动态绑定
-
class A{
-
public:
-
virtual void print(){ cout<<”This is A”<<endl;}//现在成了虚函数了
-
};
-
class B:public A{
-
public:
-
void print(){ cout<<”This is B”<<endl;}//这里需要在前面加上关键字virtual吗?(不需要,派生类直接就是虚函数)
-
};
-
int main(){
-
A a;
-
B b;
-
A* p1=&a;
-
A* p2=&b;
-
p1->print();
-
p2->print();
-
}
如上,由于基类函数是虚函数(派生类相应函数就自动变虚函数,所以派生类同名函数可以不指定为虚函数),指向不同对象的基类指针就可以调用各对象自己的函数。所以结果为:This is A,This is B;
如果上述基类的print()不是虚函数,那么结果就是This is A,This is A。
这就是虚函数在多态性继承和动态绑定方面的作用。
上述说到动态绑定,即通过基类指针对象或引用(注意:引用也可)指向派生类,调用重写的虚拟函数时直接调用被指对象(即派生类)所包含的相应虚拟函数;若调用的不是虚函数,那么直接调用基类的函数;
这里还介绍一下静态绑定,例如:((A)b).print(),输出This is A,属于静态绑定。((A)b).print()中不存在指针或引用问题,所以不是动态绑定;
其可以理解为:A temp=(A)b;temp.print();b的作用仅仅是用于初始化临时对象temp;
(2)虚函数要遵循“绝不重新定义继承而来的缺省参数”
说白了就是虚函数虽然是动态绑定的,但其参数是静态绑定(就是静态变量),只和对象指针的静态类型有关,即只可以初始化一次。
如下例:
-
-
using namespace std;
-
class A
-
{
-
public:
-
virtual void out(int i = 1)
-
{
-
cout << "class A " << i <<endl;
-
}
-
};
-
class B : public A
-
{
-
public:
-
virtual void out(int i = 2)
-
{
-
cout <<"class B " <<i <<endl;
-
}
-
};
-
int main()
-
{
-
A a;
-
B b;
-
A * p = &a;
-
p->out();
-
p->out(3);
-
p = &b;
-
p->out();
-
p->out(4);
-
B * p1 = &b;
-
p1->out();
-
p1->out(5);
-
return 0;
-
}
缺省参数是静态绑定的,pb->out()时,pb的静态类型是A*,它的缺省参数是1;但是调用的是B::out(int i);
输出:
(3)一个类中将所有的成员函数都尽可能地设置为虚函数总是有益的,但以下不可以设置为虚函数:
①只有类的成员函数才能说明为虚函数;
②静态成员函数不能是虚函数(虚函数是动态绑定的,静态函数必然不可);
③内联函数不能为虚函数(虚函数在调用中需要从虚函数表中取地址的,而内联函数是没有指定地址的);
④构造函数不能是虚函数(虚函数表是在构造函数运行时初始化(给虚函数分配地址)的,若构造函数是虚函数,那么就会出现自己在运行时才给自己分配地址,显然不可);
(4)析构函数通常声明为虚函数
因为多态(即基类对象指向派生类)情况下,若析构函数是虚函数,则对象在释放时会首先调用派生类继承的析构函数,然后再调用基类的析构函数,实现两者的同时释放。若析构函数不是虚函数,多态下对象是释放时就只会调用基类的析构函数,而造成派生类对象未释放而内存泄漏。
6、构造函数、析构函数的调用顺序
答:见如下代码:
-
//基类
-
class CPerson
-
{
-
char *name; //姓名
-
int age; //年龄
-
char *add; //地址
-
public:
-
CPerson(){cout<<"constructor - CPerson! "<<endl;}
-
~CPerson(){cout<<"deconstructor - CPerson! "<<endl;}
-
};
-
//派生类(学生类)
-
class CStudent : public CPerson
-
{
-
char *depart; //学生所在的系
-
int grade; //年级
-
public:
-
CStudent(){cout<<"constructor - CStudent! "<<endl;}
-
~CStudent(){cout<<"deconstructor - CStudent! "<<endl;}
-
};
-
//派生类(教师类)
-
//class CTeacher : public CPerson//继承CPerson类,两层结构
-
class CTeacher : public CStudent//继承CStudent类,三层结构
-
{
-
char *major; //教师专业
-
float salary; //教师的工资
-
public:
-
CTeacher(){cout<<"constructor - CTeacher! "<<endl;}
-
~CTeacher(){cout<<"deconstructor - CTeacher! "<<endl;}
-
};
-
void main()
-
{
-
CTeacher teacher;
-
}
结果:
上述例子说明:
(1)当建立一个对象时,若派生类中没有对象成员,首先调用基类的构造函数,然后调用下一个派生类的构造函数,依次类推,直至到达派生类次数最多的派生次数最多的类的构造函数为止。因为,构造函数一开始构造时,总是要调用它的基类的构造函数,然后才开始执行其构造函数体,调用直接基类构造函数时,如果无专门说明,就调用直接基类的默认构造函数。在对象析构时,其顺序正好相反。
(2)若派生类中有对象成员,首先调用基类的构造函数,然后调用下一个派生类中对象成员的构造函数,再调用该派生类的构造函数,以此类推,析构顺序正好相反。如下:
-
B:A
-
{
-
public:
-
C c;
-
}
那么构造函数调用顺序:A()->C()->B();
(3)析构函数也遵循类多态性规则:若基类析构函数是虚函数(一般都是),释放指向派生类对象的基类指针或引用时会先调用派生类析构函数释放派生类,然后再调用基类析构函数释放基类。若不是虚析构函数,就直接调用基类析构函数,而不再调用派生类析构函数。
注:根据多态性的动态绑定和静态绑定,用对象指针来调用一个函数有以下两种情况:
①如果是虚函数,会调用派生类中的版本。
②如果是非虚函数,会调用指针所指类型的实现版本。
为什么析构函数要设置成虚函数:基类析构函数是虚函数virtual,在C++中我们可以使用基类cBase的指针pBase(或引用)指向一个子类cChild,当pBase指针被撤销的时候,会先调用子类的析构函数,再调用基类的构造函数。如果不是virtual,那么撤销pBase指针时,将不会调用子类的析构函数,造成了内存泄露。
补充:
①析构函数一般都是虚函数,但构造函数不可以是虚函数;
②析构函数由于没有参数、没有返回值,所以是不可以被重载的。
7、拷贝构造函数、赋值构造函数与析构函数
答:(1)作用:
拷贝构造函数:用原对象创建并初始化新对象;
赋值构造函数:用原对象对已有的其他对象进行重新赋值;
析构函数:释放对象等作用。
注意:拷贝构造函数中创建的对象是一个实实在在的新开辟内存区域的对象,而并不是一个指向原对象的指针。
(2)声明方式:
拷贝构造函数 MyClass(const MyClass & x);
赋值构造函数 MyClass&MyClass::operator= (const MyClass & x);
析构函数 ~MyClass();
(3)注意事项:
①拷贝构造函数也是构造函数,所以没有返回值。拷贝构造函数的形参不限制为const,但是必须是一个引用,以传地址方式传递参数,否则导致拷贝构造函数无穷的递归下去,指针也不行,本质还是传值。
②赋值构造函数是通过重载赋值操作符实现的,它接受的参数和返回值都是指向类对象的引用变量。
(4)区别与共同点:
注意,拷贝构造函数和赋值构造函数的调用都是发生在有赋值运算符‘=’存在的时候,只是有一区别:
拷贝构造函数调用发生在对象还没有创建且需要创建时,如:
MyClass obj1;
MyClass obj2=obj1或MyClass obj2(obj1);
赋值构造函数仅发生在对象已经执行过构造函数,即已经创建的情况下,如:
MyClass obj1;
MyClass obj2;
obj2=obj1;
区别:拷贝构造函数就像变量初始化,赋值构造函数就如同变量赋值。前者是在用原对象创建新对象,而后者是在用原对象对已有对象进行赋值。
共同点:拷贝构造函数和赋值构造函数都是浅拷贝,所以遇到类成员含有指针变量时,类自动生成的默认拷贝构造函数和默认赋值构造函数就不灵了。因为其只可以将指针变量拷贝给新对象,而指针成员指向的还是同一内存区域,容易产生:冲突、野指针、多次释放等问题。解决方法就是自己定义具有深拷贝能力的拷贝构造函数或者赋值构造函数。
(5)拷贝与赋值构造函数内在原理(m_data是String类成员):
-
// 拷贝构造函数
-
String::String(const String &other)
-
{
-
//允许操作other 的私有成员m_data
-
int length = strlen(other.m_data);
-
m_data = new char[length+1];(1)//开辟新对象内存
-
strcpy(m_data, other.m_data);(2)//复制内容到新对象
-
}
-
// 赋值函数
-
String & String::operator =(const String &other)
-
{
-
//(1) 检查自赋值
-
if(this == &other)
-
return *this;
-
//(2) 释放原有的内存资源
-
delete [] m_data;
-
//(3)分配新的内存资源,并复制内容
-
int length = strlen(other.m_data);
-
m_data = new char[length+1];
-
strcpy(m_data, other.m_data);
-
//(4)返回本对象的引用
-
return *this;
-
}
上述是内部实现原理,可知:
①拷贝和赋值构造函数都是新开辟内存,然后复制内容进来;
②赋值构造函数一定要最先检测本操作是否为自己给自己赋值,若是就会直接返回本身。若直接从第(2)步开始就会释放掉自身,从而造成第(3)步strcpy中的other找不到内存数据,从而使得赋值操作失败。
(6)将类中的析构函数设为私有,类外就不可以自动调用销毁对象,所以只可以通过new创建对象,手动销毁。
8、关于禁用拷贝构造函数和赋值构造函数
答:(1)包括两步:
1、将两个构造函数声明为私有private;
2、仅仅声明函数就可以了,不做定义;
解释:前者保证外部不可调用,后者保证内部成员he友元不可调用,因此可实现禁用。
(2)为什么一般要禁用两个构造函数:如上所述,拷贝构造函数和赋值构造函数是都是浅拷贝,若成员含有指针,易产生冲突、野指针、多次释放等问题。所以一般直接禁用,以防不测。
(3)可不可以不禁用?可以,现在一般借助智能指针就可以不禁用。
10、浅拷贝与深拷贝
答:浅拷贝:比如拷贝类对象时,对象含有指针成员,只是拷贝指针变量自身,这样新旧对象的指针还是指向同一内存区域;
深拷贝:同理,对象含有指针成员,拷贝时不仅拷贝指针变量,还重新在内存中为新对象开辟一块内存区域,将原对象次指针成员所指向的内存数据都拷贝到新开辟的内存区域。
11、常忽视的问题——构造函数一般都要定义为公有的
答:构造函数如果被定义为私有或者不表明私有公有(编译器默认为私有),就会造成创建对象时无法调用构造函数而出错。例如:
-
-
class A
-
{
-
A()
-
{
-
printf("A()");
-
}
-
};
-
void main()
-
{
-
A a;
-
}
就会出错:error: ‘A::A()’ is private。原因就是类外无法调用私有的构造函数。
但是也有例外,比如单例模式下,构造函数就是私有的。因为单例模式下,类对象是类自己以公有成员函数模式创建的;
12、类的成员变量初始化的两种方式
答:构造函数里初始化方式和构造函数初始化列表方式;
后者效率一般高于前者(尤其是在对象指针变量初始化中),因为前者要先运行构造函数,后执行赋值操作,而后者只需要运行复制构造函数即可。
实际上,在构造函数类初始化应该叫做赋值而不是初始化。
13、必须在构造函数列表中初始化的3种情况
答:注意必须初始化就是表示:对象成员不可被修改,只可以在声明是初始化;
所以,一定包含const、引用成员。当然还包括其他的,如下:
1.带有
const
修饰的类成员 ,如
const
int
a ;
2.包含引用成员,如
int
& p;
3.类类型的成员没有默认构造函数(就是类中的另一个类类型的成员没有默认的参数为空的构造函数):
-
class a{
-
private:
-
int aa;
-
public:
-
a(int k):aa(k){
-
-
};
-
//a(){
-
//
-
//};
-
}
-
-
class b{
-
private:
-
int bb;
-
a A;
-
public:
-
b(int k):bb(k){
-
-
};
-
}
如上所说:class b中的类类型成员A,但构造函数并没有在初始化列表中显示初始化它,所以b类的构造函数只会隐私的初始化它(注意所有成员变量都会经过构造函数初始化),而隐式初始化时就相当于b(NULL):A(NULL),而a没有参数为空的默认构造函数,所以会报错。两种解决方法:
①如上注释部分,添加默认构造函数;
②使用b类的初始化列表显示初始化。
14、派生类继承问题
答:(1)不管是私有还是公有继承,基类的私有成员都是会被派生类继承的吗?
是的。派生类会继承基类的公有、私有成员和保护成员,只是根据继承方式和成员类型限制,不能访问私有等成员而已。
(2)派生类访问属性
① 基类的私有成员无论什么继承方式,在派生类中均不可以直接访问;
②在公有继承下,基类的保护成员和公有成员均保持原访问属性;
③在保护继承方式下,基类的保护和公有成员在派生类的访问属性均为保护属性;
④在私有继承下,基类的保护和公有成员在派生类中的访问属性均为私有属性。
(3)补充:除了public可以类外访问外(所谓类外访问一般就是类对象访问),其他两个都不能被类外访问;
但是protect相比private的访问权限还是大一些,因为派生类的成员函数可以访问继承而来的保护(protect)成员,而不能访问继承而来的private成员。
总结访问属性:
①private:
自己所在类的成员函数访问。被派生类继承后,派生类的成员函数不可访问它(这一点比较特殊,虽然基类私有成员在派生类中仍然为私有成员,但不可被派生类的成员函数访问)。类外(类对象或者派生类对象)均不可访问它;
②protect:
自己所在类的成员函数可访问;
被非私有继承后,派生类的成员函数可访问它;
类外(自己所在类的类对象或派生类对象)均不可访问;
③public:自己所在类的成员函数可访问;
被非私有继承后,派生类成员函数可以访问它;
自己所在类对象、公有继承的派生类对象均可访问它;
(4)禁止类被继承的方法:将类的构造函数设置为私有,这样派生类在实例化时首先要先实例化基类,但基类的构造函数私有不可被访问,所以就会出错。因此可以得出结论:类的构造函数为私有,该类就不可被继承。
15、this指针
答:this指针:类的每个成员函数都有一个this指针,其指向调用自己的类对象。this是一个指针,其值为*const类型的地址,不可改变不可被赋值。只有在成员函数中才能调用this指针。静态函数(方法)由于是所有类对象共享的,所以没有this指针,this指针本来就是成员函数的一个参数。如
MovePoint(int a,int b)函数的原型应该是 void MovePoint( Point *this, int a, int b)。
this指针的使用:一般都是隐式应用,显式应用一般为返回整个对象如return *this(*this就是所指向对象的别名)。
16、基类和派生类之间的赋值问题
答:基类对象与派生类对象之间存在赋值相容性,包括以下几种情况:
结论:基类与派生类之间的的对象强制转化一般只向上(向父辈)进行,不使用父辈向下转换。即:一般都是基类指针指向派生类对象,而非派生类指针指向基类对象,因为后者是不安全。
注:向下转换在C++中虽然不安全,但并不禁止这种做法。
17、对象数组
答:1)该数组中若干个元素必须是同一个类的若干个对象。对象数组的定义、赋值和引用与普通数组一样,只是数组的元素与普通数组不同,它是同类的若干个对象。定义例如:DATE dates[7];
表明dates是一维对象数组名,该数组有7个元素,每个元素都是类DATE的对象。
注:有人可能不同意“同类的若干个对象”,认为派生类对象也可以,但是考虑到实际中计算数组存储空间=数组长度×单个元素大小,这就要求各个元素大小相同,显然派生类对象大于基类对象,不适合作为元素。
2)对象数组可以被赋初值,也可以被赋值
例如下面是定义对象数组并赋初值和赋值:
DATE dates[4]={ DATE(7, 7, 2001), DATE(7, 8, 2001), DATE(7, 9, 2001), DATE(7, 10, 2001) }//赋初值
dates[0] = DATE(7, 7, 2001);//赋值
dates[1] = DATE(7, 8, 2001);
dates[2] = DATE(7, 9, 2001);
dates[3] = DATE(7, 10, 2001);
在建立数组时,同样要调用构造函数。如果有50个元素,就需要调用50次构造函数。在需要的时候,可以在定义数组时提供实参以实现初始化。
18、常量对象
答:常量指针指向常对象, 常对象只能调用其常成员函数。例如:
-
class A{
-
virtual void f() { cout << "A::f() "; }
-
void f() const { cout << "A::f() const "; }
-
};
-
const A* a;
-
a->f();
因此通过a->f()调用的结果是voidf() const;
19、C/C++允许多继承
答:C/C++是允许多继承的,例如:class C : public A, public B。但是Java是不允许多继承的,Java对于多继承的功能是用接口来实现的。
20、虚拟继承
答:即Derive::virtual public Base{ },这种用法主要是为了在多重继承的情况下节省空间,保证被多次继承的基类自备拷贝一份。
如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。如下图区别:
普通多继承 虚继承
21、函数模板与模板函数、类模板与模板类的介绍
答:无论是类模板或是函数模板,都是在函数或类前面加上template<class T>,然后将函数或者类中的参数类型改为T,就成为了类/函数模板。
(1)函数模板的目的:函数模板可以用来创建一个通用的函数,以支持多种不同的形参,避免重载函数的函数体重复设计。
函数模板声明格式如:
-
template<class T> T min(T x,T y)
-
{
-
函数体
-
}
具体定义如下:
-
template<class 数据类型参数标识符1,…,class 数据类型参数标识符n><返回类型><函数名>(参数表)
-
{
-
函数体
-
}
注意:函数模板最大特点是把函数使用的数据类型作为参数,有多个类型参数则每个参数前面要有class或者typename(class和typename可以混合着用)。函数模板的实例化由编译器在调用时自动完成,但下面几种情况需要程序员自己指定,急不可省略实参:
1)从模板函数实参表获得的信息有矛盾之处。
2)需要获得特定类型的返回值,而不管参数的类型如何。
3)虚拟类型参数没有出现在模板函数的形参表中。
4)函数模板含有常规形参。
(2)模板函数
在使用函数模板时,要将这个形参实例化为确定的数据类型,将类型形参实例化的参数称为模板实参,用模板实参实例化的函数称为模板函数。模板函数的生成就是将函数模板的类型形参实例化的过程。
(3)类模板
类模板定义:一个类模板(也称为类属类或类生成类)允许用户为类定义一种模式,使得类中的某些数据成员、默写成员函数的参数、某些成员函数的返回值,能够取任意类型(包括系统预定义的和用户自定义的)。
类模板格式:
-
template<class T>
-
class Test{
-
private:
-
T n;
-
const T i;
-
static T cnt;
-
public:
-
Test():i(0){}
-
Test(T k);
-
~Test(){}
-
void print();
-
T operator+(T x);
-
};
类体外的成员函数定义应如下(必须在传统定义前进行模板声明):
template <类型名 参数名1,类型名 参数名2,…>
函数返回值类型 类名<参数名 1 参数名 2,…>::成员函数名(形参表)
{
函数体
}
举例:
-
template<class T>
-
void Test<T>::print(){
-
std::cout<<"n="<<n<<std::endl;
-
std::cout<<"i="<<i<<std::endl;
-
std::cout<<"cnt="<<cnt<<std::endl;
-
}
类模板的实例化不同于函数模板自动进行,必须由程序员显示指定,格式如:Test<int> ts;
(4)模板类
模板类:就是类模板实例化后的结果。该实例化如上所述需要程序员显示指定。
22、模板的实现和声明是否一定要在同一个头文件中,为什么?
答:是的。虽然平时声明一般都是在头文件.h中,实现是在.cpp源文件中,但使用模板时C++编译器是直接到声明代码的头文件中寻找实现部分的。如果模板的声明和实现分离,那么编译不会报错但链接会报错,因为编译器在声明的头文件里面找不到实现。
至于原因:因为模板定义很特殊。由于template<…> 的参数类型在实例化前并不明确,所以编译器在不为它分配存储空间。它一直处于等待状态直到被一个模板实例告知,编译器和连接器的某一机制去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。
23、模板的特化和偏特化
答:(1)定义:特化是模板中的概念,指模板中有一些特殊类型需要单独定义的情况。
(2)特化实现:在原模板下添加一个template<>开头的同名模板,参数定义为你需要的特殊类型,内容则根据自己需求定义。
①类模板特化:例如stack类模板针对bool类型有特化,因为实际上bool类型只需要一个二进制位,就可以对其进行存储,使用一个字或者 一个字节都是浪费存储空间的。如下:
-
template <class T>
-
class stack {};
-
template < >
-
class stack<bool> { //…// };
上述template<> class stack<bool> {……}就是对stack的特化部分。
②函数模板特化:同样,函数模板特化也是针对某个特定类型的特殊处理,一个比较经典的例子:
-
template <class T>
-
T mymax(const T t1, const T t2)
-
{
-
return t1 < t2 ? t2 : t1;
-
}
-
main()
-
{
-
int highest = mymax(5,10);//正确结果
-
char c = mymax(‘a’, ’z’);//正确结果
-
const char* p1 = “hello”;
-
const char* p2 = “world”;
-
const char* p = mymax(p1,p2);//错误结果,因为比较的是指针,而不是内容
-
}
如果需要得到正确结果就需要针对const char*的函数模板特化:
-
const char* mymax(const char* t1,const char* t2)
-
{
-
return (strcmp(t1,t2) < 0) ? t2 : t1;
-
}
(3)模板偏特化
①定义:模板的偏特化是指需要根据模板的某些但不是全部的参数进行特化。
1)类模板的偏特化
例如c++标准库中的类vector的定义:
-
template <class T, class Allocator>
-
class vector { // … // };
-
template <class Allocator>
-
class vector<bool, Allocator> { //…//};
这个偏特化的例子中,一个参数被绑定到bool类型,而另一个参数仍未绑定需要由用户指定。
2)函数模板偏特化
严格的来说,函数模板并不支持偏特化,但由于可以对函数进行重载,所以可以达到类似于类模板偏特化的效果。
-
template <class T> void f(T); //(a)
-
//根据重载规则,对(a)进行重载
-
template <class T> void f(T*); //(b)
如果将(a)称为基模板,那么(b)称为对基模板(a)的重载,而非对(a)的偏特化。C++的标准委员会仍在对下一个版本中是否允许函数模板的偏特化进行讨论。
(4)模板特化时的匹配规则
1)类模板的匹配规则
最优化的优于次特化的,即模板参数最精确匹配的具有最高的优先权。例子:
-
template <class T> class vector{//…//}; // (a) 普通型
-
template <class T> class vector<T*>{//…//}; // (b) 对指针类型特化
-
template <> class vector <void*>{//…//}; // (c) 对void*进行特化
每个类型都可以用作普通型(a)的参数,但只有指针类型才能用作(b)的参数,而只有void*才能作为(c)的参数
2)函数模板的匹配规则
非模板函数具有最高的优先权。如果不存在匹配的非模板函数的话,那么最匹配的和最特化的函数具有高优先权。例子:
-
template <class T> void f(T); // (d)
-
template <class T> void f(int, T, double); // (e)
-
template <class T> void f(T*); // (f)
-
template <> void f<int> (int) ; // (g)
-
void f(double); // (h)
-
bool b;
-
int i;
-
double d;
-
f(b); // 以 T = bool 调用 (d)
-
f(i,42,d) // 以 T = int 调用(e)
-
f(&i) ; // 以 T = int* 调用(f)
-
f(d); // 调用(h)
24、友元类与友元函数
答:(1)友元类
A是B的友元类,则表示A不需要继承B类也可以访问B的成员(包括公有,保护,私有成员)。但注意,友元关系不可逆,即B不一定是A的友元类;友元关系也不可传递,即A的派生类不一定是B的友元类。若要使B为A的友元类,在A类的成员列表中定义:friend class B;
例如:
-
class TV
-
{
-
public:
-
friend class Tele;//友元类,表示tele是TV的友元类,可以访问TV的所有成员,但不是TV的派生类
-
TV():on_off(off),volume(20),channel(3),mode(tv){}
-
private:
-
enum{on,off};
-
enum{tv,av};
-
enum{minve,maxve=100};
-
enum{mincl,maxcl=60};
-
bool on_off;
-
int volume;
-
int channel;
-
int mode;
-
};
(2)友元函数
如果要在类外访问类的所有成员,或者类A中的函数要访问类B中的成员,那么该函数就应该是友元函数。如上面所述的友元类声明一样,友元函数的声明也是在需要被访问的类中声明友元函数如:friend +普通函数声明。
说明:友元类即可以声明为类的公有、也可以是私有成员,只要声明为友元函数,就可以访问类的所有成员。
友元函数声明与调用举例:
-
class B
-
{
-
friend void Print(const B& obj);//声明友元函数
-
}
-
void Print(const B& obj)
-
{
-
//函数体
-
}
-
void main()
-
{
-
B obj;
-
obj.Print(obj);//直接调用,可以访问B类中的所有成员
-
}
25、操作符重载问题
答:C++中规定,重载运算符必须和用户定义的自定义类型的对象一起使用,即重载运算的参数中必须包括用户自定义的类型对象。
C++中绝大部分的运算符可重载,有几个不能重载的运算符,分别是: . 和 .* 和 ?: 和 :: 和 sizeof。
运算符重载规则如下:
①、 C++中的运算符除了少数几个之外,全部可以重载,而且只能重载C++中已有的运算符。
②、 重载之后运算符的优先级和结合性都不会改变。
③、 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。一般来说,重载的功能应当与原有功能相类似,不能改变原运算符的操作对象个数,同时至少要有一个操作对象是自定义类型。
运算符重载为类的成员函数的一般语法形式为:
函数类型 operator 运算符(形参表) { 函数体;}
运算符重载为类的友元函数的一般语法形式为:
friend 函数类型 operator 运算符(形参表) { 函数体;}
如下:
const Point Point::operator+(const Point& p) { return Point(x+p.x); }
Point const operator-(const Point& p1,const Point& p2){ return Point(p1.x-p2.x); }
就是运算符的重载定义
注意:算符重载为类的成员函数时,形参个数比原始的少一个。
(1) 双目运算符重载为类的成员函数时,函数只显式说明一个参数,该形参是运算符的右操作数。
(2) 前置单目运算符重载为类的成员函数时,不需要显式说明参数,即函数没有形参。
(3)后置单目运算符重载为类的成员函数时,函数要带有一个整型形参(该形参无实质用处,是用于和前置区别的)。
具体可参考:http://blog.csdn.net/dingyuanpu/article/details/5852825
注意:运算符重载一般有两种:重载为类的成员函数或重载为友元函数
(1)只能使用成员函数重载的运算符有:=、()、[]、->、->*、new、delete。
(2)单目运算符最好重载为成员函数。
(3) 对于复合的赋值运算符如+=、-=、*=、/=、&=、!=、~=、%=、>>=、<<=建议重载为成员函数。
(4) 对于其它运算符,建议重载为友元函数。
更多描述见:http://blog.chinaunix.net/uid-21411227-id-1826759.html
26、抽象类
答:抽象类是含有纯虚函数的类,其作用是作为多个表象不同,但本质相同类的抽象。
故抽象类仅可以作为基类被继承,不可以实例化生成对象,不能初始化,不能被当做返回值,不能当做参数,但可以做指针类型和引用。
27、C++中箭头操作符(→)与点操作符(.)的区别?
答:箭头操作符左边必须是指针,点操作符左边必须是实体(类对象或者结构体)。举个栗子如下:
-
struct MyStruct
-
{
-
int member_a;
-
};
-
MyStruct * ps;
那么访问member_a有两种方式:(*ps).member_a = 1;或者ps->member_a = 1;
其中*ps是结构体,用点操作符;而ps是指向结构体的地址指针,需要用箭头操作符。
28、关于类成员函数的重载、覆盖和隐藏的区别
答:成员函数被重载特征是:
(1)相同的范围(在同一个类中);
(2)函数名字相同
(3)参数不同
(4)virtual 关键字可有可无
覆盖就是指派生类函数覆盖基类virtual函数,特征是:
(1)不同的范围(分别位于派生类与基类)
(2)函数名字相同
(3)参数相同
(4)基类函数必须有virtual 关键字
“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,派生对象都是调用派生类的同名函数。规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同,此时不论有无virtual关键字、基类的函数将被隐藏;
(2)如果派生类的函数与基类的函数同名,并且参数也相同、但是基类函数没有virtual 关键字,此时基类的函数被隐藏(注意别与覆盖混淆);
总结:
①同一类中的同名函数是重载;
②不同类中同名函数可能是覆盖,也可能是隐藏。根据是否有virtual以及函数参数是否相同区分;
注意:若派生类中重新定义了基类的成员变量,则在使用派生类对象调用该对象时,只要对象没有virtual修饰,调用哪个根据当前成员实际属于哪个类确定。如下:
-
class A{
-
private:
-
int k;
-
public:
-
int a;
-
A(){
-
a=1;
-
}
-
void print(){
-
printf("%d",a);
-
}
-
};
-
-
class B: public A{
-
public:
-
int a;
-
B(){
-
a=2;
-
}
-
};
-
-
int main(){
-
B b;
-
b.print();
-
printf("%d",b.a);
-
}
输出结果:12
因为print()属于A,那么其中调用的a就属于a,故为1。同理下面的printf(“%d”,b.a)中的a属于b,所以就是2.
29、智能指针类
答:(1)智能指针(smart pointer)类是存储指向动态分配(堆)对象指针的类,智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。
作用:由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete。程序员忘记 delete,流程太复杂,最终导致没有 delete,异常导致程序过早退出,智能指针就是用来有效缓解这类问题的。因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。
如原始程序:
-
void remodel(std::string & str)
-
{
-
std::string * ps = new std::string(str);
-
...
-
if (weird_thing())
-
throw exception();
-
str = *ps;
-
delete ps;
-
return;
-
}
若在delete之前发生异常,就会导致指针ps指向的内存未释放而内存泄漏。若改为使用智能指针,如下:
-
-
void remodel (std::string & str)
-
{
-
std::auto_ptr<std::string> ps (new std::string(str));
-
...
-
if (weird_thing ())
-
throw exception();
-
str = *ps;
-
// delete ps; NO LONGER NEEDED
-
return;
-
}
即使程序出现异常,只要ps指针失效(程序运行范围超出函数)就会被智能指针类自动释放。
原理:每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。
(2)c++里面的常用的智能指针包括:auto_ptr、unique_ptr、shared_ptr和weak_ptr,第一个auto_ptr已经被c++11弃用,为什么摒弃auto_ptr呢?因为auto_ptr可能会出现两个或更多智能指针指向同一目标,从而造成在结束时对同一内存指针多次释放而导致程序崩溃。share_ptr智能指针可以避免这种情况,因为share_ptr自带了引用型计数器,可以记录同一内存指针被share_ptr指针指向的次数,释放时先查看计数器的值,从而决定是否delete。unique_ptr则是可以在编译时检测出该类错误,直接不让编译通过,从而避免程序崩溃。所以相对而言auto_ptr是最容易造成程序崩溃的。
(3)智能指针类通常用类模板实现:
一般用两个类来实现智能指针的功能,其中一个类是指针类,另一个是计数类,如下:
-
template <class T>
-
class SmartPclass Counter //计数类,用于存储指向同一对象的指针数
-
{
-
private:
-
T* ptr;
-
int cnt;
-
public:
-
friend class SmartPointer;
-
Counter()
-
{
-
ptr = NULL;
-
cnt = 0;
-
}
-
Counter(T* p)
-
{
-
ptr = p;
-
cnt = 1;
-
}
-
~Counter()
-
{
-
delete ptr;
-
}
-
};
-
template <class T>
-
class SmartPointer
-
{
-
private:
-
Counter* ptr_counter;
-
public:
-
SmartPointer(Object* p)
-
{
-
ptr_counter = new Counter(p);//新创建一个指针类先初始化计数类
-
}
-
SmartPointer(const SmartPointer &sp)
-
{
-
ptr_counter = sp.ptr_counter; //拷贝操作则直接将目标对象的计数类存储值加1
-
++ptr_count->cnt;
-
}
-
SmartPointer& operator=(const SmartPointer &sp)
-
{
-
++sp.ptr_counter->cnt; //赋值操作在调用赋值构造函数前,会调用前面传统构造函数新建一个对象,所以这里对还需要对这个新创建的对象进行处理
-
--ptr_counter->cnt;
-
if (ptr_counter->cnt == 0)
-
{
-
delete ptr_counter;
-
}
-
ptr_counter = sp.ptr_counter;
-
}
-
~SmartPointer()
-
{
-
--ptr_counter->cnt;
-
if (ptr_counter->cnt == 0) //当计数减为0时,表示所有的指针都已释放,可以释放内存;
-
{
-
delete ptr_counter;
-
}
-
}
-
};
(3)智能指针应用
在C++中智能指针的引用库是<memory>,C++11后包括上述提到的后三种智能指针。常用的也就是两种:unique_ptr和share_ptr。其中后者更好用,用法如下:
-
shared_ptr<string> s1(new string);
-
shared_ptr<string> s2 = s1;
就定义并初始化了两个share_ptr型智能指针s1,s2。
30、explicit关键字的作用
答:explicit用来防止构造函数初始化的隐式转换。
发生隐式转换,除非有心利用,隐式转换常常带来程序逻辑的错误,而且这种错误一旦发生是很难察觉的。原则上应该在所有的构造函数前加explicit关键字,当你有心利用隐式转换的时候再去解除explicit,这样可以大大减少错误的发生。
-
class String{
-
explicit String(int n);
-
String(const char *p);
-
};
-
String s1 = 'a'; //错误:不能做隐式char->String转换
-
String s2(10); //可以:调用explicit String(int n);
-
String s3 = String(10);//可以:调用explicit String(int n);再调用默认的复制构造函数
-
String s4 = "Brian"; //可以:隐式转换调用String(const char *p);再调用默认的复制构造函数
-
String s5("Fawlty"); //可以:正常调用String(const char *p);
31、构造函数和析构函数可以抛出异常吗?
答:(1)构造函数可以。但是不建议抛出异常,因为抛出异常后析构函数就不会执行了,从而需要手动释放内存;
(2)析构函数不可以抛出异常,因为容易造成死循环。
①原因:C++异常处理模型是处理那些因为出现异常而失效的对象,处理方式是调用这些失效对象的析构函数,释放掉它们占用的资源。如果析构函数向外抛出异常,则异常处理代码会调用自己,然后自己又抛出异常,……陷入无尽递归嵌套之中,因此这是不被允许的。
②处理析构函数异常的正确方式:将异常封装在析构函数内部,而不是抛出异常。
第十章、string类和STL模板库
1、C++中string的find、rfind、find_first_of和find_first_not_of,以及substr函数
答:string函数库中有以上几个关于字符搜索的函数,返回地址;
常见用法:
string s,str;
s.find(str);//查找s字符串中含有的str,并返回str首字符的地址,没查找到就返回string::npos;
s.rfind(str);//反向查找s字符串中的str,并返回str首字符的地址,没查找到就返回string::npos;
s.find_first_of(str); //函数是查找s中查找与str字符串中任何一个字符匹配的字符,如果有,则返回第一个找到的字符索引,否则返回string::npos;
s.find_first_not_of(str); //函数是查找s中查找与str字符串中任何一个字符都不匹配的字符,如果有,则返回第一个找到的字符索引,否则返回string::npos;
substr(index,length); //函数是截取字符串,第一个参数为起始位置,第二个参数是长度;
2、STL标准库相关内容集中介绍
答:STL的最大特点就是:
数据结构和算法的分离,非面向对象本质。访问对象是通过象指针一样的迭代器实现的;
容器是象链表,矢量之类的数据结构,并按模板方式提供;
算法是函数模板,用于操作容器中的数据。由于STL以模板为基础,所以能用于任何数据类型和结构。
STL中六大组件:
1)容器(Container),是一种数据结构,如list,vector,和deques ,以模板类的方法提供。为了访问容器中的数据,可以使用由容器类输出的迭代器;
2)迭代器(Iterator),提供了访问容器中对象的方法。例如,可以使用一对迭代器指定list或vector中的一定范围的对象。迭代器就如同一个指针。事实上,C++的指针也是一种迭代器。但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符地方法的类对象;
3)算法(Algorithm),是用来操作容器中的数据的模板函数。例如,STL用sort()来对一个vector中的数据进行排序,用find()来搜索一个list中的对象,函数本身与他们操作的数据的结构和类型无关,因此他们可以在从简单数组到高度复杂容器的任何数据结构上使用;
4)仿函数(Function object)
5)迭代适配器(Adaptor)
6)空间配制器(allocator)
注意:本文中前三者是主要解释部分;
在C++标准中,STL被组织为下面的13个头文件:
<algorithm>、<deque>、<functional>、<iterator>、<vector>、<list>、<map>、<memory>、<numeric>、<queue>、<set>、<stack>
和<utility>。
1)C++的STL容器介绍:
STL的容器可以分为以下几个大类:
一:序列容器, 有vector, list, deque, string、array(array是C++11新增的).(该类型容器也属于STL一级容器,注:STL中一级容器是容器元素本身是基本类型,非组合类型。)
二 : 关联容器, 有set, multiset, map, mulmap, hash_set, hash_map, hash_multiset, hash_multimap
三: 其他的杂项: stack, queue, valarray, bitset
如上,我们重点关注vector,deque,list,map:
其中vector和deque内部是数组结构,也就是顺序存储结构。
deque是双端队列;
list是双向循环链表;
关联容器的元素是自动按key升序排序,所以是排好序的。例如Map、mulmap;
部分容器的介绍如下:
2)C++中的容器迭代器iterator应用以及迭代失效问题
(1)迭代器(iterator)是检查容器内元素并遍历元素的数据类型
在C++类似指针的作用,对操作容器很方便。在java和C#中直接让其代替了指针。每种容器都有自己的迭代器类型,这里以vector为例:
vector<int>::iterator iter //iter是vector<int>类型容器的迭代器
迭代器都有begin()和end()两个函数可以获取指向容器起始地址和结束地址。
iterator迭代器可以使用的操作符有:
++、--是基本的双向迭代符;
*用于取指向地址的元素;
==用于判断两个迭代器是否相等;
(2)迭代失效
vector非末尾位置进行插入或删除(erase)都会致使迭代器失效;
失效原因:
①插入操作:
因为vector 动态增加大小时,并不是在原空间后增加新空间,而是以原大小的两倍在另外配置一个较大的新空间,然
后将内容拷贝过来,接着再原内容之后构造新元素,并释放原空间,故容器原先的所有迭代器都会失效;
②删除操作:
删除操作后,被删除数据对应的迭代器及其后面的所有迭代器都会失效。最常见的解决方法:重新对迭代器赋初值,对于erase()函数,由于其返回值是下一个元素的迭代器iter,所以删除后直接使用iter = v.erase(iter)重新对iter赋值即可;
补充:而对于关联结构如链表list,哈希表map等删除一段连续元素恰恰要使用iter++。
(3)vector的访问方式
访问vector中的数据 使用两种方法来访问vector。
1、 vector::at()
2、 vector::operator[],就是比如对于vector<int> array,可以用array[1]之类的访问;
operator[]主要是为了与C语言进行兼容。它可以像C语言数组一样操作。但at()是我们的首选,因为at()进行了边界检查,如果访问超过了vector的范围,将抛出一个例外。由于operator[]不做边界检查,容易造成一些错误,所有我们很少用它。
3、vector创建二维数组,如vector<vector<int>> array,array就是一个行和列都是可变的二维数组;
4、对于vector数组长度求取,比如vector<vector<int>> array,array.size()表示二维数组行数,array[0].size()表示列数;
3)算法部分介绍:
STL算法部分主要由头文件<algorithm>, <numeric>, <functional>组成;要使用STL中的算法函数必须包含头文件<algorithm>,对于数值算法须包含<numeric>,<functional>中则定义了一些模板类,用来声明函数对象;
STL中算法大致分为四类:
非可变序列算法:指不直接修改其所操作的容器内容的算法。
可变序列算法:指可以修改它们所操作的容器内容的算法。
排序算法:包括对序列进行排序和合并的算法、搜索算法以及有序序列上的集合操作。
数值算法:对容器内容进行数值计算。
查找算法(13个):判断容器中是否包含某个值;
adjacent_find:在iterator对标识元素范围内,查找一对相邻重复元素,找到则返回指向这对元素的第一个元素的Forward
Iterator;否则返回last;
binary_search:在有序序列中查找value,找到返回true。重载的版本实用指定的比较函数对象或函数指针来判断相等;
count:利用等于操作符,把标志范围内的元素与输入值比较,返回相等元素个数;
count_if:利用输入的操作符,对标志范围内的元素进行操作,返回结果为true的个数;
equal_range:功能类似equal,返回一对iterator,第一个表示lower_bound,第二个表示upper_bound;
其他:find,find_end,find_first_of,find_if,lower_bound,upper_bound,search,search_n;
排序和通用算法(14个):提供元素排序策略;
inplace_merge:合并两个有序序列,结果序列覆盖两端范围。重载版本使用输入的操作进行排序;
merge:合并两个有序序列,存放到另一个序列。重载版本使用自定义的比较;
nth_element:将范围内的序列重新排序,使所有小于第n个元素的元素都出现在它前面,而大于它的都出现在后面。重载版本使用自定义的比较操作;
partial_sort:对序列做部分排序,被排序元素个数正好可以被放到范围内。重载版本使用自定义的比较操作;
partial_sort_copy:与partial_sort类似,不过将经过排序的序列复制到另一个容器;
其他:partition,random_shuffle,reverse,reverse_copy,rotate, rotate_copy,sort,stable_sort,stable_partition;
删除和替换算法(15个);
排列组合算法(2个):提供计算给定集合按一定顺序的所有可能排列组合;
算术算法(4个);
生成和异变算法(6个);
关系算法(8个);
集合算法(4个);
堆算法(4个);
更多介绍见:http://blog.csdn.net/xiongchao99/article/details/73694530
第十一章、输入、输出和文件
1、C语言中的scanf和printf函数
答:scanf的调用格式为:scanf(格式控制,地址表列),注意是地址表,就是说后面参数必须是地址符,和printf参数表不一样;
printf的调用格式为:printf(“格式控制字符串”,输出表列),注意是输出列表,即直接是需要输出的变量名。但有一个例外,若格式控制符为s表示字符串,即输出字符串则需输出参数是数组名(数组首地址)。
printf("%02x",xx)表示xx输出16进制,且至少要2位。若输出位数不到2位16进制,根据二进制负数高位补1,正数补0进行填充;
2、C++输出小数点后多少位
答:cout输出小数点后几位可以实现,比较麻烦,本文采用C的方式:
首先要引入库文件include<stdio.h>,然后使用printf("%.5f",d);
其中,d可以是double或者float类型。
3、printf等可变长参数函数输出问题
答:在可变长参数函数(例如printf函数)或者不带原型声明函数中,在调用该函数时C自动进行类型提升,float类型的实际参数将提升到double,但float是32位的,double是64位的,而%d只输出低32位的数据,并将这些32位二进制以十进制数输出。注意:printf()函数需要转移字符%表示输出。
4、C++中常用的字符串输入方式
答:(1)直接使用cin>>str:
char str[10];
cin>>str;//这种方式遇到空白(如空格、制表符、换行符)就结束,如:输入姓名 Bill Colintun就只会读入Bill,舍弃后半部分;
(2)cin.getline(str,20):
int size=20;
char str[size];
cin.getline(str,size);//可以读取size-1个字符,最后一个自动添加'\0',这种方只会在读取到指定数目字符或遇到换行符时才结束。
5、文件流与文件操作
答:涉及函数fopen、fclose、fprintf、fscanf、feof等。
(1)举例如下:
-
FILE *fp;
-
fp = fopen("filel.txt", "w");//打开文件,并赋予写权限,返回FILE类型的指针返回值
-
fprintf(fp, "%s%d%f\n", s, a, f)//从文件给定的FILE指针地址处fp写入
-
fclose(fp);//每一次fopen后一定要关闭
-
fp = fopen("file1.txt", "r"); //打开文件并赋予读取权限,返回FILE指针
-
fscanf( fp, "%s%s%s", str, str1, str2); //从文件指定地址fp读取
-
fclose(fp);//每一次fopen后一定要关闭
(2)feof( fp)函数
上述函数中fp指向文件的指针,feof函数的用法是从输入流读取数据,如果到达稳健末尾(遇文件结束符),返回值eof为非零值,否则为0。
第十二章、C++多线程编程
1、C++的多线程编程
答:(1)Windows下的C++多线程
首先需要引用<windows.h>库文件,创建和关闭线程如下调用:
①创建一个线程
HANDLE thread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); //括号中为参数,其中ThreadProc为该线程运行的函数
注意:一般只用到第3和第4个参数,第四个是传递给子线程的参数;
②关闭该线程
CloseHandle(thread);
代码示例:
-
-
-
using namespace std;
-
-
//线程函数
-
DWORD WINAPI ThreadProc(LPVOID lpParameter)
-
{
-
for (int i = 0; i < 100; ++ i){
-
cout << "子线程:i = " << i << endl;
-
Sleep(1000);
-
}
-
return 0L;
-
}
-
-
int main()
-
{
-
//创建一个线程
-
HANDLE thread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
-
//关闭线程
-
CloseHandle(thread);
-
//主线程的执行路径
-
for (int i = 0; i < 100; ++ i){
-
cout << "主线程:i = " << i << endl;
-
Sleep(1000);
-
}
-
return 0;
-
}
③保证同步的互斥量应用
上述示例中,并不能保证主线程和子线程交替运行,要确切保证交替运行一般要使用互斥量Mutex;
-
HANDLE CreateMutex(
-
LPSECURITY_ATTRIBUTES lpMutexAttributes,
-
BOOL bInitialOwner, // initial owner
-
LPCTSTR lpName // object name
-
);
该函数用于创造一个独占资源,第一个参数我们没有使用,可以设为NULL,第二个参数指定该资源初始是否归属创建它的进程,第三个参数指定资源的名称。
HANDLE hMutex = CreateMutex(NULL,TRUE,"screen");
这条语句创造了一个名为screen并且归属于创建它的进程的资源。
-
BOOL ReleaseMutex(
-
HANDLE hMutex // handle to mutex
-
);
该函数用于释放一个独占资源,进程一旦释放该资源,该资源就不再属于它了,如果还要用到,需要重新申请得到该资源。申请资源的函数如下:
-
DWORD WaitForSingleObject(
-
HANDLE hHandle, // handle to object
-
DWORD dwMilliseconds // time-out interval
-
);
第一个参数指定所申请的资源的句柄,第二个参数一般指定为INFINITE,表示如果没有申请到资源就一直等待该资源,如果指定为0,表示一旦得不到资源就返回,也可以具体地指定等待多久才返回,单位是千分之一秒。
具体对互斥量在多线程中应用如下:
-
-
-
using namespace std;
-
-
HANDLE hMutex;
-
//线程函数
-
DWORD WINAPI ThreadProc(LPVOID lpParameter)
-
{
-
for (int i = 0; i < 100; ++ i){
-
WaitForSingleObject(hMutex, INFINITE);
-
cout << "子线程:i = " << i << endl;
-
Sleep(1000);
-
ReleaseMutex(hMutex);
-
}
-
return 0L;
-
}
-
-
int main()
-
{
-
//创建互斥量,FALSE表示其不仅仅属于主线程(公用的)
-
hMutex = CreateMutex(NULL, FALSE,"pthread");
-
-
//创建一个线程
-
HANDLE thread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
-
//关闭线程
-
CloseHandle(thread);
-
-
//主线程的执行路径
-
for (int i = 0; i < 100; ++ i){
-
WaitForSingleObject(hMutex, INFINITE);
-
cout << "主线程:i = " << i << endl;
-
Sleep(1000);
-
ReleaseMutex(hMutex);
-
}
-
return 0;
-
}
本例程与②中比较就是在每一次线程运行前添加了互斥量,在本次线程运行结束时释放互斥量资源,从而保证两个线程的同步关系;
(2)Linux下的C++多线程
需要遵循POSIX接口,称为pthread,POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。线程库实行了POSIX线程标准通常称为Pthreads。
引入库:pthread.h
①数据类型:
pthread_t:线程句柄;
pthread_attr_t:线程属性;
②操纵函数:
pthread_create():创建一个线程;
pthread_exit():终止当前线程;
thread_cancel():中断另外一个线程的运行;
pthread_join():阻塞当前其他的线程,直到join指定的线程运行结束;
pthread_attr_init():初始化线程的属性;
thread_attr_setdetachstate():设置脱离状态的属性(决定这个线程在终止时是否可以被结合);
thread_attr_getdetachstate():获取脱离状态的属性;
pthread_attr_destroy():删除线程的属性;
thread_kill():向线程发送一个信号;
③同步函数:用于 mutex 和条件变量
thread_mutex_init() 初始化互斥锁;
thread_mutex_destroy() 删除互斥锁;
thread_mutex_lock():占有互斥锁(阻塞操作);
thread_mutex_trylock():试图占有互斥锁(不阻塞操作)。即,当互斥锁空闲时,将占有该锁;否则,立即返回;
thread_mutex_unlock(): 释放互斥锁;
pthread_cond_init():初始化条件变量;
thread_cond_destroy():销毁条件变量;
thread_cond_signal(): 唤醒第一个调用pthread_cond_wait()而进入睡眠的线程;
thread_cond_wait(): 等待条件变量的特殊条件发生;
Thread-local storage(或者以Pthreads术语,称作 线程特有数据);
thread_key_create(): 分配用于标识进程中线程特定数据的键;
thread_setspecific(): 为指定线程特定数据键设置线程特定绑定;
thread_getspecific(): 获取调用线程的键绑定,并将该绑定存储在 value 指向的位置中;
thread_key_delete(): 销毁现有线程特定数据键;
④工具函数:
thread_equal(): 对两个线程的线程标识号进行比较;
pthread_detach(): 分离线程;
pthread_self(): 查询线程自身线程标识号;
⑤通过pthread实现C++多线程:
-
-
-
-
-
void* print1(void* data){
-
printf("1 ");
-
}
-
-
void* print2(void* data){
-
printf("2 ");
-
}
-
-
void* print3(void* data){
-
printf("3 ");
-
}
-
-
int main(void){
-
pthread_t t1,t2,t3;//句柄
-
int i=10;
-
while(i--)
-
{
-
pthread_create(&t1,0,print1,NULL);//创建
-
pthread_create(&t2,0,print2,NULL);
-
pthread_create(&t3,0,print3,NULL);
-
-
pthread_join(t1,NULL);//阻塞其他线程,保证指定线程运行结束并返回值
-
pthread_join(t2,NULL);
-
pthread_join(t3,NULL);
-
}
-
printf("\n");
-
}
注:由于pthread库不是Linux系统默认的库,连接时需要使用库libpthread.a,所以在使用pthread_create创建线程时,在编译中要加-lpthread参数。
更多Linux的C++ 多线程请参考:http://www.cnblogs.com/youtherhome/archive/2013/03/17/2964195.html
2、C++线程安全问题
答:线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染;线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
1)线程安全问题都是由全局变量及静态变量引起的。若有多个线程同时对全局变量、静态变量执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全;
2)局部变量局部使用是安全的,因为每个thread 都有自己的运行堆栈,而局部变量是被拷贝副本到各自堆栈中,互不干扰;
3)标准库里面的string在多线程下不是安全的,它只在两种情况下安全:①多个线程同时读取数据是安全的;②只有一个线程在写数据是安全的;
4)MFC的CString也不是多线程安全的;
5)volatile不能保证全局整形变量是多线程安全。volatile是一个类型修饰符(type specifier),volatile变量是用于多个线程访问的变量,被设计用来修饰被不同线程访问和修改的变量。volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。。(注意,volatile变量也可以是const变量);
6)安全性:局部变量>成员变量>全局变量;
7)给出一个全局变量在多线程中的应用实例:两个线程并发执行以下代码,假设a是全局变量,初始值是1:
Void foo ( )
{
++a
printf("%d",a);
}
那么输出结果有4种可能:3 2; 2 3; 3 3; 2 2;
原因:①每个线程对a做++先读取,再++,最后写(a++或者++a之类的都不是原子操作,所以若不想被其他线程中途打断,都需要做线程同步);
②每个线程对a做printf前,先将数据读取到临时变量并压栈,再弹出打印;
③以上每一小步(例如先、再、后分3小步)完成后,下一小步进行前都可能被另一线程打断;
第十三章、其他
1、C++中的左值和右值
答:在 C中:可以放到赋值操作符=左边的是左值,可以放到赋值操作符右边的是右值。有些变量既可以当左值又可以当右值,这一说法不是很准确,左值为Lvalue,其实L代表Location,表示在内存中可以寻址,可以给它赋值(常量const类型也可以寻址,但是不能赋值),Rvalue中的R代表Read,表示可以读取它的值,不可以取地址和修改。
在 C++中,每一个表达式都会产生一个左值或者右值,相应的该表达式也就被称作“左值表达式", "右值表达式"。对于基本数据类型来说左值右值的概念和C没有太多不同,不同的地方在于自定义的类型,而且这种不同比较容易让人混淆,有两点要注意:
1) 对于基础类型,右值是不可被修改的,也不可被 const, volatile 所修饰;
2) 对于自定义的类型,右值却允许通过它的成员函数进行修改;
补充:临时值是const属性的,不可更改,所以临时值也可以是右值。
如下:
int a;
int b;
a = 3;
b = 4;
a = b;
b = a;
// 以下写法不合法。
3 = a;
a+b = 4;
如上,a,b都是左值,也可以是右值;但3、4为常量,只可为右值;a+b的结果变了成了一个临时值,临时值是const类型的,不可被赋值,故不可作为左值,所以a+b=4错误。
结论:1)左值、右值广义上都是表达式;
2)C++中左值是可以取地址的值和被赋值修改的,或者说是指表达式结束后仍然存在的值;相反右值不可取地址和修改的了,如常量或临时值;
3)++a是左值,因为a先进行自增运算,后返回自己,返回值仍然为自己本身,可以取地址和赋值,所以++a是左值;
a++先将a赋值给临时变量,然后返回临时变量(接着自己进行自增运算),临时变量不可更改,所以a++不是左值;
4)左值引用和右值引用;
记住:非常量引用必须是左值(即非常量引用必须指向左值),因为非常量引用面临被修改的可能,左值才可满足;
常量引用必须是右值(即常量引用必须指向右值),因为左值可能被修改,不满足常量要求;
例如:
const cs& ref1 = 1是对的的;
cs& ref2 = 1就是错误的。ref2是非常量引用,必须指向左值,而1明显是右值。
5)若想把左值当作右值来使用,例如一个变量的值,不再使用了,希望把它的值转移出去,C++11中的std::move就为我们提供了将左值引用转为右值引
用的方法。
2、C编程中,用指数形式书写数据
答:格式如下:
其中,指数部分必须为整数,不可是小数。
3、面向对象程序测试对象——mock
答:在为传统面向对象语言的程序做单元测试的时候,经常用到mock对象。Mock对象通过反射机制,可以访问类的所有成员(包括公有、保护和私有成员),所以mock的反射机制也就最大程度破坏了面向对象的封装性。
4、实现交换两个整型变量的值,要求不使用第三个变量
答:例如,a=5,b=3:
方法一:算术法,a=a+b;b=a-b;a=a-b;
方法二:异或法,a=a^b;b=a^b;a=a^b;
5、十进制变二进制方法
答:一般是将整数和小数部分分开转化,然后加在一起;
整数部分:十进制整数转换为二进制整数采用"除2取余,逆序排列"法。具体做法是:十进制整数除以2,可以得到一个商和余数;再把商除以2,又会得到一个商和余数,如此进行,直到商为0时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。也叫“倒序取余”。
小数部分:十进制小数转换成二进制小数采用"乘2取整,顺序排列"法。具体做法是:用2乘以十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,此时0或1为二进制的最后一位。或者达到所要求的精度为止。
6、转义字符
答:转义字符除了\n \t \\等外,还有\后面接数字的,如:
'\123'表示\ddd模式的3位八进制,ASCII码是83;(注:int m=0123,数值前有一个0,也表示123是八进制)
'\x12'表示\xdd模式的2位十六进制,ASCII码是18;
7、正则表达式
答:(1)正则表达式用处:主要用于字符串匹配、字符串合法性验证(如邮箱名合法性验证)、替换等。
(2)C++里面使用正则表达式一般有三种:C regex,C ++regex,boost regex,其处理速度依次减慢;
(3)C++中正则表达式用于邮箱合法性验证:
-
//电子邮件匹配
-
void is_email_valid(string email_address)
-
{
-
string user_name, domain_name;
-
// 正则表达式,匹配规则:
-
// 第1组(即用户名),匹配规则:0至9、A至Z、a至z、下划线、点、连字符之中的任意字符,重复一遍或以上
-
// 中间,一个“@”符号
-
// 第2组(即域名),匹配规则:0至9或a至z之中的任意字符重复一遍或以上,接着一个点,接着a至z之中的任意
-
//字符重复2至3遍(如com或cn等), 第二组内部的一组,一个点,接着a至z之中的任意字符重复2遍(如cn或fr等)
-
// 内部一整组重复零次或一次
-
regex pattern("([0-9A-Za-z\\-_\\.]+)@([0-9a-z]+\\.[a-z]{2,3}(\\.[a-z]{2})?)");
-
if ( regex_match( email_address, pattern ) )
-
{
-
cout << "您输入的电子邮件地址合法" << endl;
-
// 截取第一组
-
user_name = regex_replace( email_address, pattern, string("$1") );
-
// 截取第二组
-
domain_name = regex_replace( email_address, pattern, string("$2") );
-
cout << "用户名:" << user_name << endl;
-
cout << "域名:" << domain_name << endl<< endl;
-
}
-
else
-
{
-
cout << "您输入的电子邮件地址不合法" << endl << endl;
-
}
-
}
8、memset()函数的作用
答:(1)memset()函数原型:extern void *memset(void *buffer, int c, int size) 其中,buffer:为指针或是数组,c:是赋给buffer的值,size:是buffer的长度;
(2)作用:Memset用来将buffer开始的长为size的内存空间全部设置为字符c,一般用在对定义的字符串进行初始化为''或'/0';这个函数在socket中多用于清空数组。
举例:原型是memset(buffer, 0, sizeof(buffer)),应用如char a[100];memset(a,'/0',sizeof(a));
(3)对于常见的一个问题“memset函数能否用来初始化一个类对象”,有如下答案:
理论上是可以,但是很危险,尤其是在类中有虚函数的情况下,不可以使用memset初始化,否则会破坏原对象的虚函数表指针。总之,memset如上所述一般只用于常规的数据存储区的清零或初始化,不要用于其他。
-
const char* mymax(const char* t1,const char* t2)
-
{
-
return (strcmp(t1,t2) < 0) ? t2 : t1;
-
}
9、内存碎片
答:内存碎片分为内部碎片和外部碎片。
(1)内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间;如某一数组容量为90,但实际只可以分配8字节的倍数大小的容量即96,剩下的6个字节内存在当前程序中得不到利用也不能再次分配给其他程序,所以成为了碎片。
(2)外部碎片是因为频繁的分配与回收物理页面会导致大量的、连续且小的页面块夹杂在已分配的页面中间,就会产生外部碎片;假设申请内存块就0~9区间,继续申请一块内存为10~14区间。把第一块内存块释放,然后再申请一块大于10个单位的内存块,比如说20个单位。因为刚被释放的内存块不能满足新的请求,所以只能从15开始分配出20个单位的内存块。现在整个内存空间的状态是0~9空闲,10~14被占用,15~24被占用。其中0~9就是一个内存碎片了。如果10~14一直被占用,而以后申请的空间都大于10个单位,那么0~9就永远用不上了,变成外部碎片。
(3)如何解决内存碎片? 采用Slab Allocation机制:整理内存以便重复使用。
Slab Allocator的基本原理是按照预先规定的大小,将分配的内存分割成特定长度的块,以完全解决内存碎片问题。如下图:
每次根据需要,从上述合适大小的chunks内存组中选择一块进行存储,从而减少内部碎片产生。同时,使用结束后并不释放而是在机制下进行整理以便下次重复使用,从而又可以减少外部碎片(外部碎片主要由于频繁分配释放产生)。
10、栈溢出
答:最简单的栈溢出就是无限递归调用,即函数递归调用时没有设置终止条件,就会发生栈溢出。