有些东西自己写代码的时候可能永远都用不着,但是用来应付面试还是有用的
1.初始化
int a(3);
int a(int(23));都是可以的,
2.只有const引用可以指向不同类型的对象,只要类型转换是允许的,如:
double d=3.14;
const int &i = d;
const int &j = 1024;
但是如果改变d的值不会影响i和j,因为第二句实际等于
int temp = d;
const int &i = temp;
3.注意下面的写法
const int ival = 1024;
const int *const&ip = &ival; //少了任何一个const都是不对的
注意这里有两个const,其中一个表示&ival是常量,另一个表示指向的int数据是常量
4.c++不允许定义引用数组,如
int &iar[3] = {ix, iy, iz}; //错误
5.int i[1,2];是合法的,它代表的意思是int i[2];因为[]中的的东西被看做了逗号表达式。。。
6.typedef char *cstring;
extern const cstring cstr;
上面第二个语句相当于extern char *const cstr而不是预想的extern const char *cstr;
7.大多数二元操作符两侧表达式的计算顺序是未定义的,例如
ia[index++] < ia[index],结果是依赖于编译器的
而||,&&,逗号和?:表达式四者例外,标准保证前三者它们的计算顺序是从左至右,最后一个首先计算条件表达式的值,然后根据结果选择后面两个表达式之一求值
8.虽然0既可以赋值给int型,也能赋值给int *型,但是以下写法是错误的
i = pi = 0; //其中i是int, pi是int *
因为以上写法等价于
pi = 0;
i = pi; //这里错误,指针与int不能隐式类型转换
9.尽管有人一再吹嘘sizeof后面的括号可有可无,但是sizeof int是无法通过编译的,
因为对类型名使用sizeof的时候必须加括号
10.据c++primer说大多数位操作对有符号数符号位的影响都是未定义的
11.模板定义不只能使用类型,也能使用数据。比如bitset定义
template<size_t _Bits>
class bitset{.....};
定义对象的时候就这样bitset<100> b;
关于这两者的具体区别以后再说
12.在大部分编译器上模板类的定义和实现都必须放在同一个文件中,这就是所谓的包含编译模式。
虽然也有使用export关键字的分离编译模式,但是很多编译器都不支持,连gcc都不支持,
会提示关键字export未实现,将被忽略,所以,老老实实的用包含编译模式吧。。。
13.在c中,把局部变量定义放在函数开头或者函数中某个语句块中没有什么区别,
但是c++中可不一定。比如以下语句
if( 0 ){
vector<int> v;
}
它的反汇编代码如下
if( condition )
004012A5 cmp dword ptr [ebp-10h],0
004012A9 je main+5Fh (004012bf)
vector<int> v;
004012AB lea eax,[ebp-38h]
004012AE push eax
004012AF lea ecx,[v]
004012B2 call @ILT+250(std::vector<int,std::allocator<int> >::vector<int,std::allocator<int> >)
004012B7 lea ecx,[v]
004012BA call @ILT+270(std::vector<int,std::allocator<int> >::~vector<int,std::allocator<int> >)
004012BF //if块后面的代码
当condition不满足时不会调用vector的构造函数和析构函数,如果放在函数开头定义,则总会调用构造函数和析构函数而带来额外开销。因此能放在语句块中定义的变量尽量放在语句块中定义所带来的不仅仅是代码的清晰。
14.编译器会为类提供默认的赋值函数operator=(),它所完成的只是所有成员的拷贝。如果是指针成员,则只拷贝指针,而不重新分配指针指向的空间(这就是所谓的浅拷贝),这就会造成资源的多次引用。因此有些情况下希望禁用默认赋值函数,很简单,只要定义一个赋值函数然后将其设为私有即可。
15.vector和list的性能差别。
一般需要频繁在末尾添加元素的时候选用vector,但是也不一定,当每个元素很大的时候扩充capacity的过程中需要拷贝大量的数据,这种情况下vector就不如list快,这时可以改为让vector存储对象的指针,可以极大的提高效率。
更多详细讨论参考primer 3rd chs p214-217
16.默认构造函数
可以认为如果没有提供任何构造函数,那么编译器会自动生成一个不带任何参数的默认构造函数。
更具体的讨论参考这里《关于默认构造函数的几个错误认识 》
17.int f(int);和int f(const int);是同一个函数,不能重载;
但是int f(int *);和int f(const int *);不算同一个函数,可以重载;
int f(int &);和int f(const int &);也不算同一个函数;
因为最前面两者所接受的实参集完全相同,而后面两对则不同。
18.看看这段代码(摘自c++ primer)
void print(const string &);
void print(int );
void f()
{
extern void print(int);
print("value");
print(4);
}
编译会出错。因为局部域中生命的print隐藏了全局域中的print,而不是重载,因此上面两个print在函数f中都是不可见的。这时候调用print("value");就会出错,但是print(4);是没问题的。
那么如果使用using来声明另外一个名字空间中与本名字空间中某函数同名的函数会发生什么?记住,using只是一个声明,因此,以下这段代码
using somenamespace::print;
实际上等于把somenamespace中的所有print重载函数都声明一遍(注意using声明不能指定参数表,也就是说使用using会引入该名字空间中的所有重载函数)。如果在本名字空间中没有与其原型相同的函数则会发生重载,否则会出重定义错误
19.已知以下两个声明:
extern void f(long);
extern void f(float);
下面的调用会造成二义性
f(3.14);
不要以为3.14是浮点数会调用f(float),理解这个需要知道两点:
1.c++中浮点文字常量是使用double型存储的
2.函数调用时所有的标准转换优先级都是等价的。
而3.14作为double既可以通过标准转换转换成long也可以转换成float,因此编译器无法确定究竟该调用哪一个。
类似的,调用f(3UL);也会造成二义性错误
20.已知如下声明和定义
a.void f(int);
b.void f(int &);
c.void f(const int &);
int i; int &j = i;
再给出如下调用
f(i); //a,b
f(3); //a,c
f(j); //a,b
21.我们知道,宏定义经常有副作用,如#define min(a,b) ((a)>(b)?(b):(a))
如何去除副作用?一种方案是使用内联函数,另一种是如下定义
#define min(a, b) ({ /
typeof(a) _x = (a); /
typeof(b) _y = (b); /
_x < _y ? _x : _y; })
22.模板是一个好东西,但是有时候希望对于某些特定类型希望不使用模板,怎么办?如下:
template<typename T>
void f(T a){cout<<"template function"<<endl;}
template <> void f(int a){ cout<<"not template function"<<endl;}
这样,当参数为int时就不会调用模板函数,关键在于前面的那个"template <>"
23.返回值有时也会使用模板类型参数,但是光从函数调用无法确定返回值得类型,这就需要显式指明
tempate<typename T1, typename T2>
T1 f(T2 a){T1 b = a; return b;}
调用的时候需要加上f<int, double>(3.14);这样编译器才知道返回值是整形。当然,后面一个double可以省略
24.看下面两个函数定义
template<typename T>
void f(T a){}
void f(int a){}
如果有char c1=1,c2=2;
那么调用f(c1)和f(c1+c2)分别对应哪个函数呢?
前者对应函数1,后者对应函数2。因为前者调用2的话需要一个整型提升的过程,而调用1可以精确匹配。后者在计算c1+c2时已经进行过整型提升,结果就是int型的,因此调用1和2都可以精确匹配。当模板和普通函数都可以精确匹配时,会选用普通函数
25.不依赖于模板参数的函数调用必须在使用之前声明,
依赖于模板参数的函数调用应该在模板被实例化之前声明。
26.类成员函数如果在类的定义里面直接给出函数体,则自动成为inline函数,但是究竟是否会编译成inline函数还要看函数体的复杂程度。
补充一点,类成员函数如果声明为inline的话,必须在头文件中给出函数体,可以与类定义分开,但是必须在头文件中。
27.关于类的const
只有声明为const的成员函数才能被一个const类对象调用(有例外情况,假如一个类方法为static的,那么它也可以被const对象调用 )。
const成员函数和非const的同名同参数成员函数会发生重载,这种情况下const对象会调用const成员函数,非const对象调用非const成员函数,但是构造和析构函数例外,它们不能用const修饰,因为不管是否定义为const对象,对象的常量性都只在构造之后,析构之前保持。
如果一个类含有指针,那么在const成员函数中虽然不能修改指针的值,但是能够修改指针所指的内容。
在const函数中,有时确实需要修改某些成员变量,这种情况下只要将成员变量定义为mutable即可
28.内部类访问外部类。
只有与对象无关的成员可以直接访问。例如静态成员(包括函数和变量)、类型名、枚举值等等
而其他的与对象相关的成员必须通过对象或对象的指针来访问。比如非静态成员(函数需要对象的this指针,变量更不用说)等。
29.拷贝构造函数的参数必须声明为引用。关于原因,c++primer题解 14.5题有解释
另外如果有如下定义
class A{
public:
A(A &a){} //构造函数1
A(int i){} //构造函数2
};
那么对对象A a调用a=4会调用构造函数2, 但是对于A a,b;调用a=b并不会调用构造函数1,而仅仅是拷贝所有变量
30.对于placement new初始化的对象无须使用delete释放。如果确实需要调用析构函数,则必须手工调用。
31.在每一个return语句前都会自动插入释放局部对象的代码,这样如果一个函数中有很多return语句则会造成代码膨胀,在析构函数被声明为内联时尤其严重。解决办法就是减少return的数量或者将析构函数声明为非内联。
32.如果要使用对象数组,那么一般要为类提供缺省构造函数。一般来说,在堆上构造对象数组分为两步:1)分配实际数组,这时会将缺省构造函数应用在每个对象上。2)每个元素后来被赋予一个特定的值。
其实还有一种方法,就是预先分配空间,然后循环调用placement new为每一个对象调用构造非缺省函数。
33.想知道c++在你的构造函数里偷偷做了什么?参考《effective c++》 3rd 条款05
34.构造函数初始化表和在构造函数中赋值有何区别?
一般类对象的初始化必须放在初始化表中,const(static除外,const static成员可以就地初始化,也可以在cpp文件中初始化)和引用数据成员也必须放在初始化表中
35.记住在构造函数的初始化列表中变量的初始化顺序是按照变量在类中声明的顺序 来的。因此对于下面的类,希望将b初始化为3,再将a初始化为b是错误的
class A{
public:
int a;
int b;
A():b(3),a(b){} //错误,这里会先执行a(b),后执行b(3)。gcc使用-Wall选项会给出警告。
}
36.如果一个函数调用的实参是一个类类型的对象、类类型的指针、类类型的引用或者指向类成员的指针,则候选函数是以下几个函数集合的并集:a)在调用点可见的函数。b)在定义该类的名字空间中声明的函数。c)类成员表中声明为友元的函数。如果实参中有多个类对象,则将所有类对象的并集加起来就是候选函数集。
37.模板类的内部类可以直接使用外部类的模板参数,而且无须加上template<typename T>之类的模板声明语句。