/*
达内学习 C/C++FAQs 01 day57 2013-11-20
C/C++ 的一些常见问题
*/
一,引用
引用只是其初始化物(即目标)的别名
能对引用所作的唯一的操作就是初始化
一旦初始化结束,引用就是其目标的另一种写法
引用没有地址,对引用取地址是目标的地址。
没有引用的引用,也没有引用的指针,也没有引用数组。
int a=12;
int& r = a;
int&& rr = r; //错误
int&* pr = &r; //错误
int& ar[3]; //错误
引用不可能带有常属性(const)和挥发性(volatile)
但是引用的目标可以带有常属性和挥发性。
const int a = 12;
const int& cr = a;
//------------------------------------
volatile int a = 12;
volatile int& vr = a;
在被typedef定义的类型别名上使用const或volatile饰词,不会引编译错误,编译器会直接忽略,仅将其视为平凡引用
typedef int* PINT; //定义了一个int指针类型 是指针
const PINT p; //int* const PINT是指针
typedef int& RINT;
const RINT rc = a; // 编译器不报错 忽略const 当int&
没有空引用 也没有泛型引用
T* p = NULL; //p是空指针
T& r = *p; //引用不存在的对象,结果未定义
void& rv; //错误
引用就是(其不可变更的目标的)别名,既然是别名,必须是某个实体的别名,一定要有意义。
任何能作为左值的表达式都可以用于初始化引用,并成为引用的目标。
int& r = a[n-6][m-2]; // 引用数组元素
如果函数的返回值具有引用类型,这就意味着该函数的返回值可以作为左值使用。
引用型参数,避免以值的方式传递参数带来的拷贝开销,充当输出参数。
目标类型为用引用的类型转换,和目标类型非应用的类型转换具有完全不同的语义。
int a = 0;
char* p = reinterpret_cast<char*>(a);
//先将int类型的a复制到一个char*类型的临时变量,再将该char*类型的临时变量赋值给相同类型的p
reinterpret_cast<char*&>(a) = cp;
//将int类型的a本身解释成char* 类型,并令其接受cp的值,不产生临时变量
reinterpret_cast<char*>(a) = cp;// 错误,临时变量只能做右值
可以定义数组的引用,数组的引用,在保留其地址的同时还保留了数组的长度信息
void foo(int a[12])
{
cout<<sizeof(a)/sizeof(a[0])<<endl;// 1 传入的是指针
}
void bar(int (&a) [12])
{
cout<<sizeof(a)/sizeof(a[0])<<endl; //12
}
如同声明函数指针一样,也可以生命函数的引用。
通过函数名、函数指针、函数引用调用函数,都可以使用直接语法,解引用语法。
int func (double);
int (*pfunc) (double) = func; //函数指针
int (&pfunc) (double) = func; //函数引用
int a = func(1.2) // 直接通过函数名调用
int a = *func(1.3) // 先将函数名隐式转换成函数指针,通过解引用调用
int a = (*pfunc) (1.2);// 对函数指针解引用调用
int a = rfunc(1.2); // 通过函数引用调用
int a = (*rfunc)(1.2); // 先隐式转换成指针 再解引用
二,常属性(const)
被const修饰的变量和字面值常量的区别
int i = 12;
const int c = 12;
12 字面值常量,没有地址
i 是普通变量,有自己的地址,其值可变
c 是具有常属性的变量,有自己的地址,,其值可变,不能通过c变
const 饰词是用来约束被声明的标示符号,而非去约束该标识符所标示的对象。
const int cv = 12;
int* p = const_cast<int*> (&cv);
*p = 13;
cout<< cv <<endl; //12 编译器优化直接替换
const volatile int cv = 12; //挥发性 编译器不替换
int* p = const_cast<int*> (&cv);
*p = 13;
cout<< cv <<endl; //13 编译器不替换
int i = 12;
const int* p = &i; // 不能通过 p修改
*p = 13; // 错误!
三,运算符函数名字的查找
被重载的运算符,其本质就是可以通过中序语法调用成员函数或者全局函数。
对于支持运算符重载的类型:
class String
{
public:
String& operator=(const String&);
friend const String operator+(const String&,const String&); // 全局函数 友元
operator const char*(void) const; // 类型转换成 char*
const String operator-(void) const;
};
c = a +b;
// c.operator = (operator+(a,b));
const char* cp = a;
// 等价 const char * cp = a.operator const char* ();
中序语法和函数语法在名字查找顺序上不完全一致
函数的语法的名字查找,依据从内作用域到外作用域的标准顺序展开,只要在类的内部找到了operator+的成员函数就不会再到类的外部去找同样名为operator+的全局函数(friend),虽然从参数匹配上,后者比前者正确。
中序语法则部首函数调用沿作用域从内到外展开的限制,编译器会试图找到所有的可能支持Complex类型的加号运算的运算符函数。然后再从中选择匹配程度最高的版本。
四,临时对象
在某些特定的场合,编译器不得不创建临时对象。
int a = 10,b = 20,c = 30;
c = a+b+c;
临时对象的生存期间,从其被创建开始。直到最大可能的比和表达式结束时。
临时对象一旦被析构,一切建立在对临时对象依赖基础之上的逻辑都十分危险,
string s1("hello"),s2("world");
printf("%s\n",(const char*)(s1+s2));
s1+s2的结果被按位转储到一个临时string对象中,在print返回之前,该对象不会被析构,因此通过const char*类型转换所得到的指针是安全的
const char*p = (const char*)(s1+s2);
临时对象在完成对指针p初始化以后即被析构
printf("%s\n",p);
传递给pinrtf的p已经沦为也指针,对其目标的访问将引发未定义的行为
具名非临时变量的寿命比匿名临时变量长,只要不离开定义的作用域,都是安全的。
正确使用短命的临时变量,一方面可以提高程序的空间利用率,另一个方面可以避免当前作用域被冗余的名字污染。
C++98 之前的编译器,临时变量析构的时机不尽相同,有些编译器(vc6)在遇到临时变量的“}”时析构,令一些编译器会在其所在语句的“;”时析构。
五,字符串字面值异常
应受谴责的实践
throw “堆栈下溢!”;
异常被不惑的一句是他们的类型,而不是值,而字符串字面值形式的异常没有任何信息体现在其类型之中。
try {
}
catch (const char* e)
{
if(!strcmp(e,"堆栈下溢!"))
}
一旦修改了throw子句,与此异常有关的所有catch代码必须同步修改。
推而广之,永远不要抛出任何内奸或者标准类型的异常。
因为他们单凭类型无法表达异常的种类。
不惑异常的人也无法根据类型决定下一步该做什么。
异常的设计与面向对象的设计理念从根本上是一致的,即通过一种自恰的属性及行为的描述,表达一种抽象化的概念。
总之,让类型决定一切。
class StackUnderflow {};
try{
}
catch (StackUnderflow& e)
{
//处理堆栈下溢
}
为异常增加一些描述性的文本
class StackUnderflow
{
public:
StackUnderflow(const char* msg = "堆栈下溢":m_msg(msg){}
string m_msg;
};
throw StackUnderflow();
throw StackUnderflow("Stack Underflow!");
更好的方式是单独提供一个函数不惑描述性的信息
class StackUnderflow
{
public:
virtual const char* what(void) const throw(){}'
};
这么做为了与标准库一致
也可以直接从标准库异常中继承;
为特定的功能模块提供一个泛化的返回的异常基类,也不失为一种优雅的设计
class ContainerException
{
public:
virtual ~ContainerException(void){}
virtual const char* what(void) const throw() = 0;
};
class StackUnderflow:public ContainerException,public runtime_error
{
public:
explicit StackUnderflow(const char* msg = "堆栈下溢"):runtime_error(msg){}
const char* what(void) const throw()
{
return runtime_error::what();
}
};
注意:异常一定要支持完整意义上的拷贝。
当执行一个throw语句时,运行期异常机制会把所抛出的异常对象赋值到一个临时对象,该临时对象存在绝对安全区,存储临时异常的对象的安全区是高度平台相关,但是可以保证在最后一个使用该异常对象的catch 块执行完成之前该对象一直保持可用。
当异常发生时,没有什么地方是绝对安全的,除了存放临时异常对象的安全区,这是唯一值得信赖的区域。
由此可见将一个对象的指针作为异常抛出是不明智的。
永远不要抛出指针,要抛出对象。
StackUnderflow e;
throw e;
//-----------------------
throw StackUnderflow(); //这个好
直接抛出匿名对象,安全又简洁。
class StackUnderflow:public StackException{};
try
{}
catch (StackException e){'../.}; //不好
catch (StackException& e) {...};
1,如果实际抛出的是StackUnderflow异常,catch字句将导致异常对象被截切,子类特有部分丢失。
2,如果子类StackUnderflow 覆盖了基类StackException中的虚函数,才用第一种方式表现不出多态,但是用引用可以。
3,如果StackException是个抽象类,catch 子句会引发编译错误。
4,用对象不用引用的话无法在修改异常状态以后继续抛出。
5,用拷贝性能下降,可能引发新的异常。
鉴于上述理由,以引用的方式捕获异常是明知之举。
catch 字句从上到下遵循,从特殊到一般,从具体到抽象,从子类到基类的原则。
catch (StackUnderflow& e){}
catch (StackException& e){}
catch (exception& e){}
catch (...) {.......} // 其他异常
(*p++)++ //先算 p++
int a[]={11,22,33,44,55},*p = a;
cout<<(*p++)++<<endl; // 输出11
cout<<*p<<*--p<<--*p<<endl; // cout从右往左算
// --*p 21 *--p 12 *p 12
cout<<*p<<*p--<<(*p)--<<endl
// 12 21 22
(a+b)/2 = a&b + (a^b>>2)