1.string类的加号“+”相当于拼接的作用,a+b说明a在前面,和b+a的结果是不一样的,a+=b等价于a=a+b;
2.atoi是c++内置函数,itoa并不是,只有VS编译器环境下可以使用,需要原int型数据、char指针和目标进制数(int型)三个参数;
3.VS编译环境下不能用cout直接输出string,需要c_str()方法处理后输出,在GCC编译环境下则可以直接输出;
4.32位系统中所有的指针大小都是4B,64位都是8B;
5.vector的实质还是数组,其大小是固定的,只不过每次push_back的时候若超出内存会重新申请内存(一般为原内存的两倍),然后有个拷贝过程,这个效率比较低。
6.C++的常量字符串是存在单独的内存区域中(和储存局部变量的栈不同),如果用指针指向相同字符常量,则两个指针指向相同(为了节省内存)。但是如果用相同字符常量给两个字符数组赋值,情况则不同,两个数组会分别申请空间,其头指针指向不同。
7.new和malloc的区别:
malloc是标准库函数,new是C++运算符,malloc只能完成内存分配不能对所对获得内存进行初始化。
对象创建是需要调用构造函数,消亡是需要调用析构函数,对于自定义结构,由于malloc是标准库函数,不在编辑器的控制范围内,所以不能把调用构造函数和析构函数的任务附 加给malloc。
8.为什么构造函数不能是虚函数,而析构函数可以。
调用虚函数需要虚函数表,但是在类完成构造之前对象还没有被分配空间,自然也就不存在虚函数表。
再一个虚函数本身就是因为需求不明而暂不实现,而构造函数的存在目标很明确,就是为了初始化实例,所以从这个层面把构造函数定义成虚函数也没有意义。
而我们通常情况下把析构函数定义成虚函数是因为,很多时候我们都是通过父类指针来调用和释放子类,这时候析构函数如果不是虚函数,那么只会调用父类的析构函数不会调用 子类的析构函数。
9.关于static的初始化问题。
因为static型变量本身是静态的,像在如下函数中,
void Func()
{
const static int num=0;
cout << num << endl;
num++;
}
如果在同一函数中调用此函数两次,按照static的设计初衷就是为了num可以得到累加,所以输出的是
0
1
所以static变量的初始化(第一句)只会在编译时被执行一次。
如果num变为非const型:
void Func()
{
static int num;
num=0;
cout << num << endl;
num++;
}
这样相对容易出错些,因为非const变量没有要求声明时赋值,所以如果程序按照上面方式赋值,那么程序结果可能不是我们想要的。
0
0
这是因为虽然num初始化语句只在编译时执行了一次,但是作为赋值语句,num=0在每次调用Func()时都会被执行,所以在定义非const型变量时最好在声明时赋值。
10.虚函数的问题
c++的面试,虚函数应该是被问得最多的知识点了,做下总结。
先谈谈普通函数的调用,普通函数在编译的时候,就已经成型了(参数类型,个数,函数体),所以通过重载和模板实现的多态通常称为编译时多态。
class A
{
public:
void fun()
{
cout <<"A" << endl;
}
};
class B:public A
{
public:
void fun()
{
cout <<"B" << endl;
}
};
int main()
{
A *a=new A;
A *b=new B;
a->fun();
b->fun();
}
上述代码的结果是A,A,就是因为在编译时a,b被当做静态类型即都是A类,编译过后对应调用的fun()都是A::fun()所以结果都是A,所以要想实现运行时多态还需要虚函数才行。
而虚函数其存在是为了实现运行时多态,即在运行阶段才决定(计算出)函数的地址。虽然和编译时多态被划为两类,虚函数机制也是通过编译器来实现的。
简单来说,虚函数是为了实现:调用一个虚函数时,被执行的代码必须和调用函数对象的动态类型相一致。
大部分编译器是通过虚函数表(vtbl)和虚函数指针(vptr)来实现的。
虚函数表:当一个类中定义了虚函数,那么在构造类的时候就会生成该类自己的虚函数表,也就是说虚函数表是和类一一对应的,而不是和类实例。
虚函数表的主要内容就是该类中所有虚函数的地址。
虚函数指针:含有虚函数类的实例,除了包含变量之外,还包含指向该类虚函数表的指针,就是虚函数指针,用于找到类的虚函数表。
子类的虚函数表会包含所有父类的虚函数地址,后面还跟着自己独有的虚函数地址。
如果子类重写了父类的虚函数,那么子类虚函数表中原来父类寻函数指针的位置会被子类重写的虚函数指针所覆盖。
所以这样以后调函数,只会调到子类的函数,不会调用父类的。
如果一个子类有多个父类(多继承),那么子类的实例中会有多个vptr(个数和继承的个数相同),我的理解是这个子类有多个虚函数表(每个父类对应一个)。
这样不同的父类指针指向子类的时候,会查询不同的虚函数表来调用自己实际的虚函数。
这时候子类自己独有的虚函数一般附在第一个父类对应的虚函数表中(大多编译器是这样的,不过编译器有选择不同方式的自由)。
虚函数的调用过程:
1. 通过实例对象得到类的虚函数指针,一般为对象的前4个字节(32位系统)。
2. 通过虚函数表找到需要调用的虚函数的地址。
3. 通过地址调用虚函数。
附:
1.虚函数的缺点:
主要从指针多次偏转的代价(时间),虚函数表大小、虚函数指针大小(空间)两个方面来分析。
2.另外虚函数对安全性也提出了考验。(下面第二个链接的后半部分)
(1)子类中独有的虚函数是不能通过父类指针来调用的(编译时便会错误)。
(2)通过虚函数表可以调用父类中的non-public虚函数(通过获得public类虚函数的地址做偏移)。
(3)父类可以直接调用子类中的non-public虚函数。
class A:
{
public:
virtual fun()
{
cout << "base." << endl;
}
};
class B: public A
{
private:
virtual fun()
{
cout << "derive." << endl;
}
};
int main()
{
A *a=new B;
a->fun();
return 0;
}
上面代码的输出结果是:derive.
是什么原因呢?
这似乎要牵扯到虚函数编译时的一些过程处理。
在编译的时候(上述代码中为a->fun();)实例对象的类型是按静态类型来处理的,这时候a的类型就是A,而A中的fun()是public的,所以编译并不会发生错误。
在实际的调用中,采用的是(*a->vptr[1])(a)来调用的,即a的动态类型其实是B。
运行时获取到的是B::fun()的地址,所以输出结果是derive,这也就是动态运行的机理。
再来一个更邪乎的···
class A
{
public:
virtual void fun(int i = 1)
{
std::cout << "base fun called, " << i;
}
};
class B: public A
{
private:
virtual void fun(int i = 2)
{
std::cout << "derive fun called, " << i;
}
};
int main()
{
A *a=new B;
a->fun();
return 0;
}
按照之前的解释,输出的字符部分是derive fun called没问题,可是后面跟的数字会是什么呢?
答案竟然是1!而解释呢?
其实这是一种不规范的代码实现,effective c++中要求我们绝不重新定义继承而来的默认参数,说的就是以上的这样情况。
因为虚函数的动态绑定的,而默认参数(缺省参数)是静态绑定的。
所以最后函数的调用方式其实是(a->vptr[1])(a,1),答案自然是像上面所说的那样,但是这样的程序真的很容易给人误导。
具体的图片不太好画,可以参照一下两篇博客。
11.联合体和结构体
说实在的,大二学的c,现在早忘了联合体是什么了,今天查了查。
联合体:可以包含几种不同的数据类型,共用一段内存,所以说联合体的大小应该是其中最大的数据类型的大小。
结构体:可以包含几种不同的数据类型,不同的类型是同时存在的,不相互影响,所以说结构体的大小应该是所有数据类型内存对齐后的总大小。
union un
{
int a;
char b[2];
};
如上联合,int占4个字节,char数组占2个字节,所以联合体大小为4字节,windows x86编译器为是小端模式,所以其内存结构应该是:
a
b[0] b[1]
* * * *
un tmp;
tmp.a=0x12345678;
现在我们做如上赋值,则内存的实际情况如下:(一个16进制数占2bit,一个char可以接受两个16进制数)
a
b[0] b[1]
78 56 34 12所以此时如果我们输出
printf("%x\n",tmp.b[0]);
会输出78
tmp.b[0]=0x34;
修改之后,输出tmp.a
printf("%x\n",tmp.a);
会输出12345634。
12.大小端
大端:数据的低位存放在内存的高位
小端:数据的低位存放在内存的低位
int a = 0x12345678;
地址---->递增
大端:12 34 56 78小端:78 56 34 12
可以看见大端模式其实是更符合我们的直观想法的,而Windows x86编译器为小端模式。