C++笔试题汇编
【内存中的字节对齐】
CPU的优化规则是这样的:对于n(n=2,4,8…)字节的元素,它的首地址能被n整除,才能获得最好的性能。数据对齐,是指数据所在的内存地址必须是该数据长度的整数倍。比如DWORD数据的内存起始地址能被4除尽,WORD数据的内存起始地址能被2除尽。x86 CPU能直接访问对齐的数据,当它试图访问一个未对齐的数据时,会在内部进行一系列的调整。
结构体或类的长度一定是最长数据元素的整数倍。如下面这个结构体占12个字节:
struct test{
char x1; //4 bytes,为了使int对齐到其自然边界,后面补3个字节
int y; //4 bytes
char x2; //2 bytes,为了使short对齐到其自然边界,后面补1个字节
short z; //2 bytes
};
空类占1个字节,但如果该类中含有虚函数,则虚函数表占4个字节,如下面这个类占4个字节:
class A{
public:
void f(){}
virtual void g(){} //4 bytes,虚函数表(虚指针)
}
【类型转换】
C++定义了一组内置类型对象之间的标准转换,在必要时它们被编译器隐式地应用到对象上。隐式类型转换发生在下列这些典型情况下:
Ø 在混合类型的算术表达式中:最宽的数据类型成为目标转换类型,这也被称为算术转换(Arithmetic Conversion)。
Ø 用一种类型的表达式赋值给另一种类型的对象:在这种情况下目标转换类型是被赋值对象的类型。
Ø 调用函数时,将实参表达式传递给形参,此时,目标转换类型是形参的类型。
Ø 函数返回时,将返回表达式自动转换为函数类型。
算术转换保证了二元操作符的两个操作数被提升为共同的类型,然后再用它表示结果的类型。两个通用的指导原则如下:
1、为防止精度损失,如果有必要的话,类型总是被提升为较宽的类型。
2、所有含有小于整型的有序类型的算术表达式在计算之前类型都会被转换成整型。编译器将在所有小于int的整值类型上施加整型提升(integral promotion),在进行整值提升时类型char、signed char、unsigned char和short int都被提升为类型int。【自增运算符】
int a = 4;
(A)a += (a++); (B) a += (++a) ;(C) (a++) += a;(D) (++a) += (a++);
上面4个表达式的书写是否正确,如果正确,a的取值为多少?
(A) 9
(B) 10
(C) 后置自增运算符返回右值,而赋值操作符的左操作数要求左值;
(D) 11,前置自增运算符返回对象本身。
【与零值比较】
(1) 布尔变量与零值比较:if(flag) if(!flag)(2) 整型变量与零值比较:if(value == 0) if(value != 0)
(3) 浮点型变量与零值比较:if( (x >= -0.00001) && (x<=0.00001) )
(4) 指针变量与零值比较:if(p==NULL) if(p != NULL)
【const常量与宏常量】
(1) cosnt常量: const T var = value;(2) 宏常量:#define var value
(3) 区别:const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查;而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的边际效应。
(4) 如果在类中定义了const数据成员,只能在构造函数的初始化列表来完成初始化。并且const数据成员只在某个对象生存期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同对象其const数据成员的值可以不同。
class A{
public:
A();
private:
const int m_data; //如果写成const int m_data = 5 非法
};
A::A():m_data(5){};
(5) 如果要建立在整个类中都恒定的常量,可以使用枚举常量来实现。
class A{
private:
enum {SIZE1 = 100, SIZE2 = 200};
int array1[SIZE1];
int array2[SIZE2];
};
【const的用途】
const除了定义常量以外,还可以用来修饰函数的输入参数,返回值,甚至函数的定义体。(1) 如果输入参数采用指针传递,那么加const修饰可以防止意外地改动该指针。例如:
char * strcpy(char *strDest, const *strSrc);
(2) 如果输入参数采用值传递,将自动产生临时变量用于复制该参数。若参数是类类型对象,那么采用const引用传递的方式可以节省临时对象的构造、复制、析构过程。
void Func(const A &a);
注意,对内置类型的输入参数,没有必要将值传递的方式改为const引用传递。
(3) 如果函数的返回值采用指针传递,加const修饰,那么返回值的内容不能被修改,该返回值只能赋给另外一个加const修饰的同类型指针。
const char* GetString(void);
const char *str = GetString();//ok
char *str = GetString();//非法
特别注意,
不要返回指向栈内存的指针,在函数结束时,栈内存会被回收。但可以返回指向堆内存的指针。
(4) 如果函数的返回值采用值传递, 加const修饰没有任何价值。函数结束时会把返回值复制到外部临时存储单元中(然后销毁栈内存)。
(5) 如果函数的返回值采用引用传递,这种场合并不多见,一般用于类的复制函数,目的是为了实现链式表达。
(6) 修饰类的成员函数,const成员函数不能修改数据成员,也不能调用非const成员函数。
【引用与指针的区别】
(1) 引用被创建的同时必须进行初始化,指针则可以在任何时候被初始化。
(2) 不能有NULL引用,引用必须与合法的存储单元关联;指针则可以是NULL。
(3) 引用一旦被初始化,就不能改变引用的关系;指针则可以随时改变所指的对象。
【拷贝构造函数的三种使用情形】
(1) 用类的一个对象去初始化同类的另一个对象。
(2) 如果函数的形参是类的对象,调用函数时,进行形参和实参的结合时。
(3) 如果函数的返回值是类的对象,函数执行完成返回调用者时。
class A{
public:
A(int a){m_data = a;}
A(A &other);
void print1(){cout<<m_data<<endl;}
private:
int m_data;
};
A::A(A &other){
m_data = other.m_data;
cout<<"copy constructor is called."<<endl;
}
void print2(A a){
a.print1();
}
A print3(A &a){
return a;
}
int main(){
A a(5);
print2(a); //类类型的对象作为函数参数
A b(a); //初始化一个对象
a = print3(b); //返回一个类类型的对象
return 0;
}
【析构函数的无限递归】 下面这个类在调用析构函数时会引发无限递归!
class A{
public:
A *p;
A(){p = this;}
~A(){if(p!=NULL){delete p; p=NULL;}}
};
我们知道,析构函数中在执行delete p这条语句的时候,由于p指向对象自身,释放p所指向的内存时又调用了析构函数,而析构函数中又要指向delete语句,造成了无限递归调用。此外,需要注意的是,如果类中含有指针,最好不要使用默认的big-three,浅拷贝是不安全的。