C++学习之五、理解C++疑难问题

来源:岁月联盟 编辑:exp 时间:2011-12-01

 

理解C++疑难问题

 

1.  引用

 

          专业的C++代码都大量使用了引用。C++的引用是另外一个变量的别名。对引用的修改都会改变该引用所指向变量的值。可以把引用看成是一种隐式的指针,它可以免除获取变量地址和对指针解除引用的麻烦。也可以把引用看作是原变量的另一个名字。可以创建独立的引用变量、使用类中的引用数据成员、接受作为传递给函数和方法的参数、从函数和方法返回引用。

 

(1).引用变量:必须在创建时对其初始化。

 

int x = 3;

 

int &xRef = x;

 

xRef = 10;//x的值修改为10

 

如果在类外声明的引用变量而不初始化,这是不允许的。

 

int &yRef; //error

 

必须在分配引用时对其初始化。通常,引用是在声明是分配的,不过引用数据成员可以在包含该成员的类的初始化列表中进行初始化。

 

除非引用指向一个const值,否则不能创建指向未命名值的引用。

 

int &unnameRef = 5 ; //error

 

const int &unnameRef = 5; ok

 

引用总是指向初始化时指定的那个变量。一旦创建引用,就不能再修改了。

 

int x=3,y=5;

 

int &xRef = x;

 

xRef = y; //xRef引用没有指向y,仍然是指向x,只是x的值修改成y的值5了。

 

你或许希望在赋值时取y的地址来绕过这条限制:

 

int x=3,y=5;

 

int &xRef = x;

 

xRef = &y; //error ,y的地址是指针,而xRef是int变量的引用,而不是指针的引用

 

对于下面会怎么样呢?

 

int x=3,y=5;

 

int &xRef = x;

 

int &yRef = y;

 

xRef = yRef; //xRef引用没有指向y,仍然是指向x,只是x的值修改成y的值5了。

 

综上所述:引用在初始化后就不能修改而指向别的变量,只能修改其指向的变量的值。

 

 

 

指针引用和引用指针:

 

下面是一个指向int指针的引用的例子

 

int *intP;

 

int *&ptrRef = intP

 

ptrRef = new int;

 

*ptrRef = 5;

 

注意,取引用的地址和其引用所指向的变量的地址,这二者结果是一样的。

 

int x =3;

 

int &xRef = x;

 

int *xPtr = &xRef;//等价于int *xPtr = &x

 

*xPtr = 100;

 

注意:不能声明指向引用的引用,也不能声明引用指针(指向引用的指针)。

 

int x =3;

 

int &xRef = x;

 

int &&doubleRef = xRef;//error

 

int &*refPtr = &xRef;//error

 

 

 

(2).引用数据成员:

 

类的数据成员可以是引用。但是如果引用不指向其他某个变量,这样的引用是无法存在的。因此,必须在构造函数初始化列表中初始化引用数据成员,而不是在构造函数体中完成初始化。

 

 

 

(3).引用参数:

 

C++不常使用独立的引用变量或者引用数据成员。引用最通常的用法是作为函数和方法的参数。

 

void swap(int& first,int& second)

 

{

 

  int temp = first;

 

  first = second;

 

  second = temp;

 

}

 

而下面的函数达不到效果

 

void swap(int first,int second)

 

{

 

  int temp = first;

 

  first = second;

 

  second = temp;

 

}

 

我们知道不能使用常量来初始化引用变量,与此类似,不能把常量作为实参传递给采用传引用为参数的函数。

 

swap(3,5); //error

 

来自指针的引用:如果把一个指针传递给函数或方法,而该函数或方法需要的是一个引用。在此种情况下,简单地对指针进行解除引用,从而把指针转化为引用。

 

int x = 3,y=5;

 

int *xp = &x,*yp = &y;

 

swap(*xp,*yp);

 

传值和传引用:

 

如果想修改参数,并希望这些修改反映到函数或方法的实参变量上,此时就应该采用传引用。但是,不应该限制为只是在这种情况下才采用传引用。传引用可以避免复制函数实参,在某些情况下能带来二个好处:

 

a. 效率。复制大的对象和结构时可能会花费很长的时间。传引用只向函数或方法传递指向对象或结构的指针。

 

b. 正确性。不是所有的对象都允许传值。即使允许传值,也不见得就能正确地支持深复制。我们知道要支持深复制,有动态分配内存的对象必须提供定制的复制构造函数。

 

如果想发挥这二个优点,同时不想改变原来的对象,可以在前面加上const。

 

传引用的这些优点意味着,对于简单内置类型,不需要修改实参,就应当使用传值。在其他的情况下可以考虑传引用。

 

 

 

(4).引用返回类型:

 

从函数或方法返回引用。这样做的主要原因是出于效率的考虑。不是返回一个完整的对象,而是从函数或方法返回对象的引用,这样记忆可以避免不必须要的复制。当然,只能当前对象在函数或方法结束仍然存在才可以使用此技术。

 

注意:必要返回函数或方法中在栈上创建的变量的引用。因为函数或方法在结束时会撤销这些变量。函数中在堆上分配的变量在函数结束时会撤销吗?

 

 

 

(5).采用引用还是指针:

 

C++中的引用大概是多余的,引用可以做的,几乎指针都可以做。

 

不过引用比指针编写的代码要清晰一些,也要安全,不可能存在无效的引用,不需要明确地解除引用,所以不会遇到指针可能存在的解除引用错误。

 

需要改变指着指向的位置的情况下,需要使用指针。

 

要看参数和返回类型中是采用指针还是采用引用合适,有一种方法,就是考虑谁拥有内存。如果收到变量的代码要负责释放与对象关联的内存,就必须接受对象的指针。如果收到变量的代码不必释放内存,就应该接受变量引用。即,除非需要动态分配内存或者要在其他地方改变或释放指针指向的值,否则,都应当使用引用而不是指针。(这条规则也适用于独立变、函数或方法参数、函数或方法返回值。

 

 

 

关键字疑点:

 

1.const关键字:

 

指定或者要求其声明的变量不变。

 

const有二种不同但相关的用法,一种标识变量,一种标识方法。

 

a.const变量:声明此变量不能修改。可以把任何变量标识为const,包括全局变量和类的数据成员。也可以使用const来指定函数或方法的参数应该保持不变。

 

const double PI = 3.14159;//等价于#define PI 3.14159

 

const指针:

 

int x = 5;

 

const int * p = &x; //不能通过指针p来修改x的值,但是可以通过x自身来修改。

 

*p = 10;//error

 

x =10; //ok

 

int const *p = &x; //等价于const int * p = &x

 

对于:

 

int x = 5,y = 8;

 

int *const p = &x;//可以通过p来修改x的值,但是不能修改p指向的对象了。

 

*p = 10;//ok

 

p = &y; //error

 

既然不能修改p本身,所以需要在声明p时对其初始化。

 

对于:

 

const int * const p = &x;// 既不能通过指针p来修改x的值, 也不能修改p指向的对象。

 

b.const 引用:

 

应用于引用的const关键字通常比应用于指针const关键字要简单。原因有二,一:引用默认就是const的,也就是说不能修改它们指示的变量(即不能让它再指示别的变量)。所以,C++不允许显式地用const来标识引用变量(即如,int & const xRef = x)。二:引用一般只是一个间接层。不能创建对引用的引用。要得到多重间接层(间接引用),唯一的办法就是创建指针的引用。

 

因此,我们谈到的const引用,其实是指:

 

int z;

 

const int &zRef = z; 等价于int const &zRef = z;

 

zRef = 4; //error

 

z = 4;//ok

 

const引用最常见就是作为函数或方法的参数。

 

注意:把对象作为参数传递时,默认的做法应该是传递const引用。只有确实需要改变传递过来的对象时才应该去掉const。

 

c.const方法

 

用来声明方法不能修改类中不可变的数据成员。

 

 

 

 

 

2. 关键字static

 

C++中的static关键字有三种,而且看起来不相关的用法。

 

a.static数据成员和方法

 

它们不属于某个对象,而是属于这个类。

 

b.static连接

 

C++每个源文件都是独立编译的,得到的对象文件要连接在一起。C++源文件中的每个名字,包括函数和全局变量,都有一个连接,可能是内部(internal)连接,也可能是外部(external)连接。外部连接是指,对于其他源文件,这个名字是可用的。内部连接(也称为静态连接(static linkage))是指,对于其他源文件,这个名字不可用。函数和全局变量默认都有外部连接。但是,可以在声明前面加上关键字static,来指定内部(静态)连接。

 

//FirstFile.cpp

 

void f();

 

int main()

 

{

 

  f();

 

return 0;

 

}

 

给出了f()的原型,但没有定义

 

// AntherFile.cpp

 

#include<iostream>

 

using namespace std;

 

void f();

 

void f()

 

{

 

   cout<<”f/n”<<endl;

 

}

 

给出了f()的原型和定义。

 

需要说明的是,在二个不同的文件中编写同一个函数的原型是合法的。如果每个源文件都用#include包含了一个头文件,并把方法的原型放在这个头文件中,预处理所做的正是这个工作,其作用就是在不同的源文件中有同一个方法的原型。使用头文件的原因是维护原型的副本(并保持同步更新)更为容易。不过,对于这个例子没有使用头文件。

 

这些文件编译与连接都能通过,因为f()有外部连接,main()函数可以从不同的文件调用它。

 

然而,如果在AntherFile.cpp文件中对方法f()使用static关键字:

 

// AntherFile.cpp

 

#include<iostream>

 

using namespace std;

 

static void f();

 

void f()

 

{

 

   cout<<”f/n”;

 

}

 

现在,尽管编译每个源文件时都没有问题,但是连接不会成功,因为方法f()使用内部链接,这样源文件FirstFile.cpp中就不能使用这个方法了。定义了static方法,但是在源文件中没有使用,有些编译器会发出警告。

 

注意,此时在f()的定义前面不需要重复关键字static。

 

要达到上诉的内部(静态)连接的效果,还有一种方法就是:采用匿名命名空间。即把变量和函数包装在一个未命名的命名空间中,而不是使用static关键字。

 

//AntherFile.cpp

 

#include<iostream>

 

using namespace std;

 

 

 

namespace {

 

void f();

 

 

 

void f()

 

{ cout<<”f/n”;}

 

}

 

声明了匿名命名空间中的实体之后,可以在同一源文件中的任意位置访问这些实体,但是在其他的源文件中不能访问。

 

c. 函数中的static变量

 

此种用法是创建局部变量,只在进入和退出变量作用域之间维护变量的值。函数内部的静态变量就像只能是只能从该函数访问的全局变量一样。静态变量的一种通常用法是“记住”是否都有已经为一个函数完成特定的初始化。

 

void performTask()

 

{

 

   static bool inited = false;

 

   if(!inited)

 

   {   cout<<”initing/n”; inited = true;}

 

}

 

然而,static变量往往让人很糊涂,通常还有更好的方法来建立代码,而避免使用static变量。在这种情况下,可能想在编写类时,编写一些构造函数来完成所需的初始化工作。

 

要避免使用独立的static变量。应在对象内维护变量状态。

 

 

 

3. 关键字extern

 

它看起来与static相对立的,extern用来为声明外部链接。比如,对于const与typedef默认的都有内部链接,所以可以用extern为其指定外部链接。

 

把一个名字指定为extern时,编译器会把它当作声明而不会定义来对待。意味着编译器不会为其分配空间。必须为变量提供没有关键字extern的另外的定义。

 

//AntherFile.cpp

 

extern int x;

 

int x = 3; //等价于extern int x = 3;

 

上面的文件中可以不用extern,因为x默认的就有外部链接。

 

在下面文件中使用

 

//FirstFile.cpp

 

#include<iostream>

 

using namespace std;

 

 

 

extern int x;

 

int main()

 

{ cout<<”x = ”<<x<<endl; return 0;}

 

如果此文件不用extern会导致连接失败,因为全局作用域内有二个x变量。

 

不过我们建议,尽可能不要使用全剧变量。全局变量容易让人迷惑,也很容易出错,尤其是在大型程序中。要完成这样一些功能,应该使用static类成员和方法。

 

 

 

4. 非局部变量的初始化顺序

 

程序中的全局变量和static类数据成员都是在main()函数开始运行前初始化的。给定源文件,会按照它们在该文件中出现的顺序初始化的。

 

然而,C++并没有指定也不能保证不同源文件中非局部变量的初始化顺序。

 

 

 

类型和类型强制转换:

 

typedef

 

为已有类型提供了一个新的名字。而并没有创建新类型-只是提供了引用原类型的新方法。

 

最常见的用法就是,当实际的类型名很麻烦的时候可以为其提供一个可管理的名字。

 

类型强制转换:

 

在C中使用()进行强制转换,C++中提供了四种新的类型强制转换方法:static_cast、dynamic_cast、const_cast、reinterpret_cast。应该多使用C++风格的类型强制转换。因为C++风格的类型强制转换会完成更多的类型检查。

 

const_cast:可以去除变量的常量性。这是这四种中唯一允许去除变量的常量性的类型强制转换。从理论上讲,应该不会需要进行const类型强制转换。如果变量声明为const,应该保持其不变。但是有时发现这种情况:函数指定一个const变量,但是接着这个const变量必须传递给一个取非const变量的函数。正确的做法是在程序中保持const的一致性,但是并不能总是这样,尤其是使用第三方的库时更是这样。因此,有时需要用这种类型强制转换。

 

void g(char *str)

 

{}

 

void f(const char *str)

 

{ g(const_cast<char*>(str));}

 

 

 

static_cast:

 

可以使用static_cast来显示完成C++语言直接支持的转换。

 

int i = 3;

 

double result = static_cast<double>(i);

 

也可以使用static_cast来显示地完成用户定义构造函数或者转换例程所允许的转换。

 

比如:类A有一个构造函数,这个构造函数取类B的一个对象,那么可以使用static_cast把B对象转换为一个A对象。然而,在需要进行这种转换的大部分情况下,编译器都会自动完成转换。

 

另一种用法是在继承层次结构中完成向下类型强制转换。

 

class Base

 

{

 

  public:

 

      Base(){}

 

      virtual ~Base(){}

 

};

 

class Derived:public Base

 

{

 

   public:

 

      Derived(){}

 

      virtual ~Derived(){}

 

};

 

int main()

 

{

 

   Base *b;

 

   Derived *d = new Derived();

 

   b = d; //会自动向上转换

 

   d = static_cast<Derived*>(b); //需要提供static_cast

 

  

 

   Base base;

 

   Derived derived;

 

   Base & br = base;

 

   Derived& dr = static_cast<Derived&>(br);

 

   return 0;

 

}

 

这种类型强制转换可以应用于指针,引用,但是不能处理对象本身。static_cast这种转换也不会完成运行时类型检查。

 

static_cast不能直接把一种类型的指针转换为另一种无关的类型。不能使用static_cast把指针转换为int。不能使用static_cast直接把一种类型的对象转换为另一种对象。不能使用static_cast把一个const类型强制转换为非const类型。任何没有意义的转换,static_cast都做不到。

 

 

 

reinterpret_cast:功能比static_cast强,但安全性更低。

 

可以把一种类型的指针强制转换为另外一种类型的指针,即使在继承结构它们之间不相关也可以。类似的,可以把一种类型的引用强制转换为另外一种类型的引用,即使这二种引用不相关也可以。还可以把指针转换为int,或者把int转换为指针。使用reinterpret_cast时要格外小心,因为它会把原始的位解释为不同类型,而不完成任何类型检查。

 

 

 

dynamic_cast:使用dynamic_cast进行类型强制转换时,会在继承层次结构中对类型强制转换完成类型检查。可以使用dynamic_cast来对指针或引用进行强制类型转换。dynamic_cast会在运行时检查底层对象的运行时类型信息。如果类型强制转换没有意义,dynamic_cast会返回NULL(对指针转换),或者抛出bad_cast异常(对于引用转换)。

 

class Base

 

{

 

  public:

 

      Base(){}

 

      virtual ~Base(){}

 

};

 

class Derived:public Base

 

{

 

   public:

 

      Derived(){}

 

      virtual ~Derived(){}

 

};

 

int main()

 

{

 

   Base *b;

 

   Derived *d = new Derived();

 

   b = d; //会自动向上转换

 

   d = dynamic_cast<Derived*>(b); //需要提供dynamic_cast

 

  

 

   Base base;

 

   Derived derived;

 

   Base & br = base;

 

   try{

 

   Derived& dr = dynamic_cast<Derived&>(br);

 

}catch(bad_cast&){cout<<” bad_cast!”<<endl;}

 

return 0;

 

}

 

 

 

作用域解析操作符:

 

首先在最内层检查要访问的的名字,然后再逐渐向外,直到全局作用域。不再任何名空间、函数或者类中的名字都在全局作用域中。

 

有时候,一些作用域中的名字会隐藏其他作用域中同样的名字。

 

还有时候,你在此作用域中不是想访问默认作用域的此名字,而是想访问别的作用域同样的名字,就要使用作用域解析操作符::,为每个名字限定一个作用域。

 

注意:全局作用域是未命名的,所以如果要访问全局作用域中的名字,就直接单独使用::,而不需要在前面加上作用域名称。

 

 

 

头文件:头文件中要避免同一个文件的循环引用和多重包含。使用#ifndef机制可以用于避免循环包含和多重包含。

 

 

 

//logger.h

 

#ifndef __LOGGER__

 

#define __LOGGER__

 

#include “Preferences.h”

 

class Logger{};

 

#endif  //__LOGGER__

 

要避免头文件的这些问题,另一种做法就是超前引用。

 

 

 

C中实用的工具

 

1. 变长函数参数列表 如:print()

 

void debugOut(char *str,...);

 

...表示任意数量和类型的参数。要访问这些参数,必须使用在<cstdarg>中定义的宏。可以声明va_list类型的变量,并通过调用va_start()来初始化该变量。va_stat()的第二个参数必须是参数列表中最右边的命名变量。所有函数都至少需要一个命名参数。在这个参数结束之后,它调用va_end()来结束对变长参数列表的访问。在调用va_start()之后必须调用va_end()来确保函数调用栈最后保持一致状态。

 

尽量不用此方法:因为不知道参数个数,不知道参数类型。

 

2. 预处理宏

 

#define SQUARE(x) ((x)*(x)) //注意预处理宏一定多用小括号

 

作为经验,尽量不用宏取代内联。很容易出错,不进行类型检查,还可能会带来调试错误(因为你编写的代码不是编译器看到的代码)。

 



摘自 我和我追逐的梦