介绍
曾经碰到过让你迷惑不解、类似于int * (* (*fp1) (int) ) [10];这样的变量声明吗?本文将由易到难,一步一步教会你如何理解这种复杂的C/C++声明:我们将从每天都能碰到的较简单的声明入手,然后逐步加入const修饰符和typedef,还有函数指针,最后介绍一个能够让你准确地理解任何C/C++声明的“右左法则”。需要强调一下的是,复杂的C/C++声明并不是好的编程风格;我这里仅仅是教你如何去理解这些声明。注意:为了保证能够在同一行上显示代码和相关注释,本文最好在至少1024x768分辨率的显示器上阅读。
基础
让我们从一个非常简单的例子开始,如下:
int n;
这个应该被理解为“declare n as an int”(n是一个int型的变量)。
接下去来看一下指针变量,如下:
int *p;
这个应该被理解为“declare p as an int *”(p是一个int *型的变量),或者说p是一个指向一个int型变量的指针。我想在这里展开讨论一下:我觉得在声明一个指针(或引用)类型的变量时,最好将*(或&)写在紧靠变量之前,而不是紧跟基本类型之后。这样可以避免一些理解上的误区,比如:
int* p,q;
第一眼看去,好像是p和q都是int*类型的,但事实上,只有p是一个指针,而q是一个最简单的int型变量。
我们还是继续我们前面的话题,再来看一个指针的指针的例子:
char **argv;
理论上,对于指针的级数没有限制,你可以定义一个浮点类型变量的指针的指针的指针的指针...
再来看如下的声明:
int RollNum[30][4];
int (*p)[4]=RollNum;
int *q[5];
这里,p被声明为一个指向一个4元素(int类型)数组的指针,而q被声明为一个包含5个元素(int类型的指针)的数组。
另外,我们还可以在同一个声明中混合实用*和&,如下:
int **p1; // p1 is a pointer to a pointer to an int.
int *&p2; // p2 is a reference to a pointer to an int.
int &*p3; // ERROR: Pointer to a reference is illegal.
int &&p4; // ERROR: Reference to a reference is illegal.
注:p1是一个int类型的指针的指针;p2是一个int类型的指针的引用;p3是一个int类型引用的指针(不合法!);p4是一个int类型引用的引用(不合法!)。
const修饰符
当你想阻止一个变量被改变,可能会用到const关键字。在你给一个变量加上const修饰符的同时,通常需要对它进行初始化,因为以后的任何时候你将没有机会再去改变它。例如:
const int n=5;
int const m=10;
上述两个变量n和m其实是同一种类型的--都是const int(整形恒量)。因为C++标准规定,const关键字放在类型或变量名之前等价的。我个人更喜欢第一种声明方式,因为它更突出了const修饰符的作用。
当const与指针一起使用时,容易让人感到迷惑。例如,我们来看一下下面的p和q的声明:
const int *p;
int const *q;
他们当中哪一个代表const int类型的指针(const直接修饰int),哪一个代表int类型的const指针(const直接修饰指针)?实际上,p和q都被声明为const int类型的指针。而int类型的const指针应该这样声明:
int * const r= &n; // n has been declared as an int
这里,p和q都是指向const int类型的指针,也就是说,你在以后的程序里不能改变*p的值。而r是一个const指针,它在声明的时候被初始化指向变量n(即r=&n;)之后,r的值将不再允许被改变(但*r的值可以改变)。
组合上述两种const修饰的情况,我们来声明一个指向const int类型的const指针,如下:
const int * const p=&n // n has been declared as const int
下面给出的一些关于const的声明,将帮助你彻底理清const的用法。不过请注意,下面的一些声明是不能被编译通过的,因为他们需要在声明的同时进行初始化。为了简洁起见,我忽略了初始化部分;因为加入初始化代码的话,下面每个声明都将增加两行代码。
char ** p1; // pointer to pointer to char
const char **p2; // pointer to pointer to const char
char * const * p3; // pointer to const pointer to char
const char * const * p4; // pointer to const pointer to const char
char ** const p5; // const pointer to pointer to char
const char ** const p6; // const pointer to pointer to const char
char * const * const p7; // const pointer to const pointer to char
const char * const * const p8; // const pointer to const pointer to const char
注:p1是指向char类型的指针的指针;p2是指向const char类型的指针的指针;p3是指向char类型的const指针;p4是指向const char类型的const指针;p5是指向char类型的指针的const指针;p6是指向const char类型的指针的const指针;p7是指向char类型const指针的const指针;p8是指向const char类型的const指针的const指针。
typedef的妙用
typedef给你一种方式来克服“*只适合于变量而不适合于类型”的弊端。你可以如下使用typedef:
typedef char * PCHAR;
PCHAR p,q;
这里的p和q都被声明为指针。(如果不使用typedef,q将被声明为一个char变量,这跟我们的第一眼感觉不太一致!)下面有一些使用typedef的声明,并且给出了解释:
typedef char * a; // a is a pointer to a char
typedef a b(); // b is a function that returns
// a pointer to a char
typedef b *c; // c is a pointer to a function
// that returns a pointer to a char
typedef c d(); // d is a function returning
// a pointer to a function
// that returns a pointer to a char
typedef d *e; // e is a pointer to a function
// returning a pointer to a
// function that returns a
// pointer to a char
e var[10]; // var is an array of 10 pointers to
// functions returning pointers to
// functions returning pointers to chars.
typedef经常用在一个结构声明之前,如下。这样,当创建结构变量的时候,允许你不使用关键字struct(在C中,创建结构变量时要求使用struct关键字,如struct tagPOINT a;而在C++中,struct可以忽略,如tagPOINT b)。
typedef struct tagPOINT
{
int x;
int y;
}POINT;
POINT p; /* Valid C code */
函数指针
函数指针可能是最容易引起理解上的困惑的声明。函数指针在DOS时代写TSR程序时用得最多;在Win32和X-Windows时代,他们被用在需要回调函数的场合。当然,还有其它很多地方需要用到函数指针:虚函数表,STL中的一些模板,Win NT/2K/XP系统服务等。让我们来看一个函数指针的简单例子:
int (*p)(char);
这里p被声明为一个函数指针,这个函数带一个char类型的参数,并且有一个int类型的返回值。另外,带有两个float类型参数、返回值是char类型的指针的指针的函数指针可以声明如下:
char ** (*p)(float, float);
那么,带两个char类型的const指针参数、无返回值的函数指针又该如何声明呢?参考如下:
void * (*a[5])(char * const, char * const);
“右左法则”[重要!!!]
The right-left rule: Start reading the declaration from the innermost parentheses, go right, and then go left. When you encounter parentheses, the direction should be reversed. Once everything in the parentheses has been parsed, jump out of it. Continue till the whole declaration has been parsed.
这是一个简单的法则,但能让你准确理解所有的声明。这个法则运用如下:从最内部的括号开始阅读声明,向右看,然后向左看。当你碰到一个括号时就调转阅读的方向。括号内的所有内容都分析完毕就跳出括号的范围。这样继续,直到整个声明都被分析完毕。
对上述“右左法则”做一个小小的修正:当你第一次开始阅读声明的时候,你必须从变量名开始,而不是从最内部的括号。
下面结合例子来演示一下“右左法则”的使用。
int * (* (*fp1) (int) ) [10];
阅读步骤:
1. 从变量名开始 -------------------------------------------- fp1
2. 往右看,什么也没有,碰到了),因此往左看,碰到一个* ------ 一个指针
3. 跳出括号,碰到了(int) ----------------------------------- 一个带一个int参数的函数
4. 向左看,发现一个* --------------------------------------- (函数)返回一个指针
5. 跳出括号,向右看,碰到[10] ------------------------------ 一个10元素的数组
6. 向左看,发现一个* --------------------------------------- 指针
7. 向左看,发现int ----------------------------------------- int类型
总结:fp1被声明成为一个函数的指针,该函数返回指向指针数组的指针.
再来看一个例子:
int *( *( *arr[5])())();
阅读步骤:
1. 从变量名开始 -------------------------------------------- arr
2. 往右看,发现是一个数组 ---------------------------------- 一个5元素的数组
3. 向左看,发现一个* --------------------------------------- 指针
4. 跳出括号,向右看,发现() -------------------------------- 不带参数的函数
5. 向左看,碰到* ------------------------------------------- (函数)返回一个指针
6. 跳出括号,向右发现() ------------------------------------ 不带参数的函数
7. 向左,发现* --------------------------------------------- (函数)返回一个指针
8. 继续向左,发现int --------------------------------------- int类型
总结:??
还有更多的例子:
float ( * ( *b()) [] )(); // b is a function that returns a
// pointer to an array of pointers
// to functions returning floats.
void * ( *c) ( char, int (*)()); // c is a pointer to a function that takes
// two parameters:
// a char and a pointer to a
// function that takes no
// parameters and returns
// an int
// and returns a pointer to void.
void ** (*d) (int &,
char **(*)(char *, char **)); // d is a pointer to a function that takes
// two parameters:
// a reference to an int and a pointer
// to a function that takes two parameters:
// a pointer to a char and a pointer
// to a pointer to a char
// and returns a pointer to a pointer
// to a char
// and returns a pointer to a pointer to void
float ( * ( * e[10])
(int &) ) [5]; // e is an array of 10 pointers to
// functions that take a single
// reference to an int as an argument
// and return pointers to
// an array of 5 floats.
本C/C++头文件一览
C、传统 C++
#include <assert.h> //设定插入点
#include <ctype.h> //字符处理
#include <errno.h> //定义错误码
#include <float.h> //浮点数处理
#include <fstream.h> //文件输入/输出
#include <iomanip.h> //参数化输入/输出
#include <iostream.h> //数据流输入/输出
#include <limits.h> //定义各种数据类型最值常量
#include <locale.h> //定义本地化函数
#include <math.h> //定义数学函数
#include <stdio.h> //定义输入/输出函数
#include <stdlib.h> //定义杂项函数及内存分配函数
#include <string.h> //字符串处理
#include <strstrea.h> //基于数组的输入/输出
#include <time.h> //定义关于时间的函数
#include <wchar.h> //宽字符处理及输入/输出
#include <wctype.h> //宽字符分类
//////////////////////////////////////////////////////////////////////////
标准 C++ (同上的不再注释)
#include <algorithm> //STL 通用算法
#include <bitset> //STL 位集容器
#include <cctype>
#include <cerrno>
#include <clocale>
#include <cmath>
#include <complex> //复数类
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <deque> //STL 双端队列容器
#include <exception> //异常处理类
#include <fstream>
#include <functional> //STL 定义运算函数(代替运算符)
#include <limits>
#include <list> //STL 线性列表容器
#include <map> //STL 映射容器
#include <iomanip>
#include <ios> //基本输入/输出支持
#include <iosfwd> //输入/输出系统使用的前置声明
#include <iostream>
#include <istream> //基本输入流
#include <ostream> //基本输出流
#include <queue> //STL 队列容器
#include <set> //STL 集合容器
#include <sstream> //基于字符串的流
#include <stack> //STL 堆栈容器
#include <stdexcept> //标准异常类
#include <streambuf> //底层输入/输出支持
#include <string> //字符串类
#include <utility> //STL 通用模板类
#include <vector> //STL 动态数组容器
#include <cwchar>
#include <cwctype>
using namespace std;
//////////////////////////////////////////////////////////////////////////
C99 增加
#include <complex.h> //复数处理
#include <fenv.h> //浮点环境
#include <inttypes.h> //整数格式转换
#include <stdbool.h> //布尔环境
#include <stdint.h> //整型环境
#include <tgmath.h> //通用类型数学宏
C++中的虚函数(virtual function)
1.简介
虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次:
class A
{
public:
virtual void foo() { cout << "A::foo() is called" << endl;}
};
class B: public A
{
public:
virtual void foo() { cout << "B::foo() is called" << endl;}
};
那么,在使用的时候,我们可以:
A * a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。
虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是虚函数,但它不是多态的:
class A
{
public:
virtual void foo();
};
class B: public A
{
virtual void foo();
};
void bar()
{
A a;
a.foo(); // A::foo()被调用
}
1.1 多态
在了解了虚函数的意思之后,再考虑什么是多态就很容易了。仍然针对上面的类层次,但是使用的方法变的复杂了一些:
void bar(A * a)
{
a->foo(); // 被调用的是A::foo() 还是B::foo()?
}
因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,无从确定这里被调用的是A::foo()还是B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调用,如果a指向的是B类的实例,则B::foo()被调用。
这种同一代码可以产生不同效果的特点,被称为“多态”。
1.2 多态有什么用?
多态这么神奇,但是能用来做什么呢?这个命题我难以用一两句话概括,一般的C++教程(或者其它面向对象语言的教程)都用一个画图的例子来展示多态的用途,我就不再重复这个例子了,如果你不知道这个例子,随便找本书应该都有介绍。我试图从一个抽象的角度描述一下,回头再结合那个画图的例子,也许你就更容易理解。
在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个类层次的使用者在使用它们的时候,如果仍然在需要基类的时候写针对基类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变(增加了新类),都需要使用者“知道”(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合,有人把这种情况列为程序中的“bad smell”之一。
多态可以使程序员脱离这种窘境。再回头看看1.1中的例子,bar()作为A-B这个类层次的使用者,它并不知道这个类层次中有多少个类,每个类都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来后,bar()也不需要“知道”(修改)。这完全归功于多态--编译器针对虚函数产生了可以在运行时刻确定被调用函数的代码。
1.3 如何“动态联编”
编译器是如何针对虚函数产生可以再运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman在深度探索C++对象模型[1]中的不同章节讲到了几种方式,这里把“标准的”方式简单介绍一下。
我所说的“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写,针对1.1中的例子:
void bar(A * a)
{
a->foo();
}
会被改写为:
void bar(A * a)
{
(a->vptr[1])();
}
因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个foo()函数。
虽然实际情况远非这么简单,但是基本原理大致如此。
1.4 overload和override
虚函数总是在派生类中被改写,这种改写被称为“override”。我经常混淆“overload”和“override”这两个单词。但是随着各类C++的书越来越多,后来的程序员也许不会再犯我犯过的错误了。但是我打算澄清一下:
override是指派生类重写基类的虚函数,就象我们前面B类中重写了A类中的foo()函数。重写的函数必须有一致的参数表和返回值(C++标准允许返回值不同的情况,这个我会在“语法”部分简单介绍,但是很少编译器支持这个feature)。这个单词好象一直没有什么合适的中文词汇来对应,有人译为“覆盖”,还贴切一些。
overload约定成俗的被翻译为“重载”。是指编写一个与已有函数同名但是参数表不同的函数。例如一个函数即可以接受整型数作为参数,也可以接受浮点数作为参数。
2. 虚函数的语法
虚函数的标志是“virtual”关键字。
2.1 使用virtual关键字
考虑下面的类层次:
class A
{
public:
virtual void foo();
};
class B: public A
{
public:
void foo(); // 没有virtual关键字!
};
class C: public B // 从B继承,不是从A继承!
{
public:
void foo(); // 也没有virtual关键字!
};
这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字。
2.2 纯虚函数
如下声明表示一个函数为纯虚函数:
class A
{
public:
virtual void foo()=0; // =0标志一个虚函数为纯虚函数
};
一个函数声明为纯虚后,纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有这个函数。
2.3 虚析构函数
析构函数也可以是虚的,甚至是纯虚的。例如:
class A
{
public:
virtual ~A()=0; // 纯虚析构函数
};
当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子:
class A
{
public:
A() { ptra_ = new char[10];}
~A() { delete[] ptra_;} // 非虚析构函数
private:
char * ptra_;
};
class B: public A
{
public:
B() { ptrb_ = new char[20];}
~B() { delete[] ptrb_;}
private:
char * ptrb_;
};
void foo()
{
A * a = new B;
delete a;
}
在这个例子中,程序也许不会象你想象的那样运行,在执行delete a的时候,实际上只有A::~A()被调用了,而B类的析构函数并没有被调用!这是否有点儿可怕?
如果将上面A::~A()改为virtual,就可以保证B::~B()也在delete a的时候被调用了。因此基类的析构函数都必须是virtual的。
纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。
2.4 虚构造函数?
构造函数不能是虚的。
3. 虚函数使用技巧 3.1 private的虚函数
考虑下面的例子:
class A
{
public:
void foo() { bar();}
private:
virtual void bar() { ...}
};
class B: public A
{
private:
virtual void bar() { ...}
};
在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者protected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情况,也不会发生B::bar()对A::bar()的override不起作用的情况。
这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。
3.2 构造函数和析构函数中的虚函数调用
一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。例如:
class A
{
public:
A() { foo();} // 在这里,无论如何都是A::foo()被调用!
~A() { foo();} // 同上
virtual void foo();
};
class B: public A
{
public:
virtual void foo();
};
void bar()
{
A * a = new B;
delete a;
}
如果你希望delete a的时候,会导致B::foo()被调用,那么你就错了。同样,在new B的时候,A的构造函数被调用,但是在A的构造函数中,被调用的是A::foo()而不是B::foo()。
3.3 多继承中的虚函数 3.4 什么时候使用虚函数
在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法,就可以将对象的行为抽象化。
以设计模式[2]中Factory Method模式为例,Creator的factoryMethod()就是虚函数,派生类override这个函数后,产生不同的Product类,被产生的Product类被基类的AnOperation()函数使用。基类的AnOperation()函数针对Product类进行操作,当然Product类一定也有多态(虚函数)。
另外一个例子就是集合操作,假设你有一个以A类为基类的类层次,又用了一个std::vector<A *>来保存这个类层次中不同类的实例指针,那么你一定希望在对这个集合中的类进行操作的时候,不要把每个指针再cast回到它原来的类型(派生类),而是希望对他们进行同样的操作。那么就应该将这个“一样的操作”声明为virtual。
现实中,远不只我举的这两个例子,但是大的原则都是我前面说到的“如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的”。这句话也可以反过来说:“如果你发现基类提供了虚函数,那么你最好override它”。
4.参考资料
[1] 深度探索C++对象模型,Stanley B.Lippman,侯捷译
[2] Design Patterns, Elements of Reusable Object-Oriented Software, GOF
C++程序设计从零开始之表达式
本篇是此系列的开头,在学英语时,第一时间学的是字母,其是英语的基础。同样,在C++中,所有的代码都是通过标识符(Identifier)、表达式(Expression)和语句(Statement)及一些必要的符号(如大括号等)组成,在此先说明何谓标识符。
标识符
标识符是一个字母序列,由大小写英文字母、下划线及数字组成,用于标识。标识就是标出并识别,也就是名字。其可以作为后面将提到的变量或者函数或者类等的名字,也就是说用来标识某个特定的变量或者函数或者类等C++中的元素。
比如:abc就是一个合法的标识符,即abc可以作为变量、函数等元素的名字,但并不代表abc就是某个变量或函数的名字,而所谓的合法就是任何一个标识符都必须不能以数字开头,只能包括大小写英文字母、下划线及数字,不能有其它符号,如,!^等,并且不能与C++关键字相同。也就是我们在给一个变量或函数起名字的时候,必须将起的名字看作是一个标识符,并进而必须满足上面提出的要求。如12ab_C就不是一个合法的标识符,因此我们不能给某个变量或函数起12ab_C这样的名字;ab_12C就是合法的标识符,因此可以被用作变量或函数的名字。
前面提到关键字,在后续的语句及一些声明修饰符的介绍中将发现,C++提供了一些特殊的标识符作为语句的名字,用以标识某一特定语句,如if、while等;或者提供一些修饰符用以修饰变量、函数等元素以实现语义或给编译器及连接器提供一些特定信息以进行优化、查错等操作,如extern、static等。因此在命名变量或函数或其他元素时,不能使用if、extern等这种C++关键字作为名字,否则将导致编译器无法确认是一个变量(或函数或其它C++元素)还是一条语句,进而无法编译。
如果要让某个标识符是特定变量或函数或类的名字,就需要使用声明,在后续的文章中再具体说明。
数字
C++作为电脑编程语言,电脑是处理数字的,因此C++中的基础东西就是数字。C++中提供两种数字:整型数和浮点数,也就是整数和小数。但由于电脑实际并不是想象中的数字化的(详情参见《C++从零开始(三)》中的类型一节),所以整型数又分成了有符号和无符号整型数,而浮点数则由精度的区别而分成单精度和双精度浮点数,同样的整型数也根据长度分成长整型和短整型。
要在C++代码中表示一个数字,直接书写数字即可,如:123、34.23、-34.34等。由于电脑并非以数字为基础而导致了前面数字的分类,为了在代码中表现出来,C++提供了一系列的后缀进行表示,如下:
u或U 表示数字是无符号整型数,如:123u,但并不说明是长整型还是短整型
l或L 表示数字是长整型数,如:123l;而123ul就是无符号长整型数;而34.4l就是长双精度浮点数,等效于双精度浮点数
i64或I64 表示数字是长长整型数,其是为64位操作系统定义的,长度比长整型数长。如:43i64
f或F 表示数字是单精度浮点数,如:12.3f
e或E 表示数字的次幂,如:34.4e-2就是0.344;0.2544e3f表示一个单精度浮点数,值为254.4
当什么后缀都没写时,则根据有无小数点及位数来决定其具体类型,如:123表示的是有符号整型数,而12341434则是有符号长整型数;而34.43表示双精度浮点数。
为什么要搞这么多事出来,还分什么有符号无符号之类的?这全是因为电脑并非基于数字的,而是基于状态的,详情在下篇中将详细说明。
作为科学计算,可能经常会碰到使用非十进制数字,如16进制、8进制等,C++也为此提供了一些前缀以进行支持。
在数字前面加上0x或0X表示这个数字是16进制表示的,如:0xF3Fa、0x11cF。而在前面加一个0则表示这个数字是用8进制表示的,如: 0347,变为十进制数就为231。但16进制和8进制都不能用于表示浮点数,只能表示整型数,即0x34.343是错误的。
字符串
C++除了提供数字这种最基础的表示方式外,还提供了字符及字符串。这完全只是出于方便编写程序而提供的,C++作为电脑语言,根本没有提供字符串的必要性。不过由于人对电脑的基本要求就是显示结果,而字符和字符串都由于是人易读的符号而被用于显示结果,所以C++专门提供了对字符串的支持。
前面说过,电脑只认识数字,而字符就是文字符号,是一种图形符号。为了使电脑能够处理符号,必须通过某种方式将符号变成数字,在电脑中这通过在符号和数字之间建立一个映射来实现,也就是一个表格。表格有两列,一列就是我们欲显示的图形符号,而另一列就是一个数字,通过这么一张表就可以在图形符号和数字之间建立映射。现在已经定义出一标准表,称为ASCII码表,几乎所有的电脑硬件都支持这个转换表以将数字变成符号进而显示计算结果。
有了上面的表,当想说明结果为“A”时,就查ASCII码表,得到“A”这个图形符号对应的数字是65,然后就告诉电脑输出序号为65的字符,最后屏幕上显示“A”。
这明显地繁杂得异常,为此C++就提供了字符和字符串。当我们想得到某一个图形符号的ASCII码表的序号时,只需通过单引号将那个字符括起来即可,如:'A',其效果和65是一样的。当要使用不止一个字符时,则用双引号将多个字符括起来,也就是所谓的字符串了,如:"ABC"。因此字符串就是多个字符连起来而已。但根据前面的说明易发现,字符串也需要映射成数字,但它的映射就不像字符那么简单可以通过查表就搞定的,对于此,将在后续文章中对数组作过介绍后再说明。
操作符
电脑的基本是数字,那么电脑的所有操作都是改变数字,因此很正常地C++提供了操作数字的一些基本操作,称作操作符(Operator),如:+ - * / 等。任何操作符都要返回一个数字,称为操作符的返回值,因此操作符就是操作数字并返回数字的符号。作为一般性地分类,按操作符同时作用的数字个数分为一元、二元和三元操作符。
一元操作符有:
+ 其后接数字,原封不动地返回后接的数字。如: +4.4f的返回值是4.4;+-9.3f的返回值是-9.3。完全是出于语义的需要,如表示此数为正数。
- 其后接数字,将后接的数字的符号取反。如: -34.4f的返回值是-34.4;-(-54)的返回值是54。用于表示负数。
! 其后接数字,逻辑取反后接的数字。逻辑值就是“真”或“假”,为了用数字表示逻辑值,在 C++中规定,非零值即为逻辑真,而零则为逻辑假。因此3、43.4、'A'都表示逻辑真,而0则表示逻辑假。逻辑值被应用于后续的判断及循环语句中。而逻辑取反就是先判断“!”后面接的数字是逻辑真还是逻辑假,然后再将相应值取反。如:
!5的返回值是0,因为先由5非零而知是逻辑真,然后取反得逻辑假,故最后返回0。
!!345.4的返回值是1,先因345.4非零得逻辑真,取反后得逻辑假,再取反得逻辑真。虽然只要非零就是逻辑真,但作为编译器返回的逻辑真,其一律使用1来代表逻辑真。
~ 其后接数字,取反后接的数字。取反是逻辑中定义的操作,不能应用于数字。为了对数字应用取反操作,电脑中将数字用二进制表示,然后对数字的每一位进行取反操作(因为二进制数的每一位都只能为1或0,正好符合逻辑的真和假)。如~123的返回值就为-124。先将123 转成二进制数01111011,然后各位取反得10000100,最后得-124。
这里的问题就是为什么是8位而不是16位二进制数。因为123小于128,被定位为char类型,故为8位(关于char是什么将下篇介绍)。如果是~123ul,则返回值为4294967172。
为什么要有数字取反这个操作?因为CPU提供了这样的指令。并且其还有着很不错且很重要的应用,后面将介绍。
关于其他的一元操作符将在后续文章中陆续提到(但不一定全部提到)。
二元操作符有:
+
-
*
/
%
其前后各接一数字,返回两数字之和、差、积、商、余数。如:
34+4.4f的返回值是38.4;3+-9.3f的返回值是-6.3。
34-4的返回值是30;5-234的返回值是-229。
3*2的返回值是6;10/3的返回值是3。
10%3的返回值是1;20%7的返回值是6。
&&
|| 其前后各接一逻辑值,返回两逻辑值之“与”运算逻辑值和“或”运算逻辑值。如:
'A'&&34.3f的返回值是逻辑真,为1;34&&0的返回值是逻辑假,为0。
0||'B'的返回值是逻辑真,为 1;0||0的返回值是逻辑假,为0。
&
|
^ 其前后各接一数字,返回两数字之“与”运算、“或”运算、“异或”运算值。如前面所说,先将两侧的数字转成二进制数,然后对各位进行与、或、异或操作。如:
4&6的返回值是4,4转为00000100,6转为00000110各位相与得,00000100,为4。
4|6的返回值是6,4转为00000100,6转为00000110各位相或得,00000110,为6。
4^6的返回值是2,4转为00000100,6转为00000110各位相异或得,00000010,为2。
>
<
==
>=
<=
!= 其前后各接一数字,根据两数字是否大于、小于、等于、大于等于、小于等于及不等于而返回相应的逻辑值。如:
34>34的返回值是0,为逻辑假;32<345的返回值为1,为逻辑真。
23>=23和23>=14的返回值都是1,为逻辑真;54<=4的返回值为0,为逻辑假。
56==6的返回值是0,为逻辑假;45==45的返回值是1,为逻辑真。
5!=5的返回值是0,为逻辑假;5!=35的返回值是真,为逻辑真。
>>
<< 其前后各接一数字,将左侧数字右移或左移右侧数字指定的位数。与前面的 ~、&、|等操作一样,之所以要提供左移、右移操作主要是因为CPU提供了这些指令,主要用于编一些基于二进制数的算法。
<<将左侧的数字转成二进制数,然后将各位向左移动右侧数值的位数,如:4,转为00000100,左移2位,则变成00010000,得16。
>>与<<一样,只不过是向右移动罢了。如:6,转为00000110,右移1位,变成00000011,得3。如果移2位,则有一位超出,将截断,则6>>2的返回值就是00000001,为1。
左移和右移有什么用?用于一些基于二进制数的算法,不过还可以顺便作为一个简单的优化手段。考虑十进制数3524,我们将它左移2位,变成 352400,比原数扩大了100倍,准确的说应该是扩大了10的2次方倍。如果将3524右移2位,变成35,相当于原数除以100的商。
同样,前面4>>2,等效于4/4的商;32>>3相当于32/8,即相当于32除以2的3次方的商。而4<<2等效于4*4,相当于4乘以2的2次方。因此左移和右移相当于乘法和除法,只不过只能是乘或除相应进制数的次方罢了,但它的运行速度却远远高于乘法和除法,因此说它是一种简单的优化手段。
, 其前后各接一数字,简单的返回其右侧的数字。如:
34.45f,54的返回值是54;-324,4545f的返回值是4545f。
那它到底有什么用?用于将多个数字整和成一个数字,在《C++从零开始(四)》中将进一步说明。
关于其他的二元操作符将在后续文章中陆续提到(但不一定全部提到)。
三元操作符只有一个,为?:,其格式为:<数字1>?<数字2>:<数字3>。它的返回值为:如果<数字1>是逻辑真,返回<数字2>,否则返回<数字3>。如:
34?4:2的返回值就是4,因为34非零,为逻辑真,返回4。而0?4:2的返回值就是2,因为0为逻辑假,返回2。
表达式
你应该发现前面的荒谬之处了——12>435返回值为0,那为什么不直接写0还吃饱了撑了写个12>435在那?这就是表达式的意义了。
前面说“>”的前后各接一数字,但是操作符是操作数字并返回数字的符号,因为它返回数字,因此可以放在上面说的任何一个要求接数字的地方,也就形成了所谓的表达式。如:23*54/45>34的返回值就是0,因为23*54的返回值为1242;然后又将1242作为“/”的左接数字,得到新的返回值27.6;最后将27.6作为“>”的左接数字进而得到返回值0,为逻辑假。
因此表达式就是由一系列返回数字的东西和操作符组合而成的一段代码,其由于是由操作符组成的,故一定返回值。而前面说的“返回数字的东西”则可以是另一个表达式,或者一个变量,或者一个具有返回值的函数,或者具有数字类型操作符重载的类的对象等,反正只要是能返回一个数字的东西。如果对于何谓变量、函数、类等这些名词感到陌生,不需要去管它们,在后继的文章中将会一一说明。
因此34也是一个表达式,其返回值为34,只不过是没有操作符的表达式罢了(在后面将会了解到34其实是一种操作符)。故表达式的概念其实是很广的,只要有返回值的东西就可以称为表达式。
由于表达式里有很多操作符,执行操作符的顺序依赖于操作符的优先级,就和数学中的一样,*、/的优先级大于+、-,而+、-又大于>、<等逻辑操作符。不用去刻意记住操作符的优先级,当不能确定操作符的执行顺序时,可以使用小括号来进行指定。如:
((1+2)*3)+3)/4的返回值为3,而1+2*3+3/4的返回值为7。注意3/4为0,因为3/4的商是0。当希望进行浮点数除法或乘法时,只需让操作数中的某一个为浮点数即可,如:3/4.0的返回值为0.75。
& | ^ ~等的应用
前面提过逻辑操作符“&&”、“||”、“!”等,作为表示逻辑,其被C++提供一点都不值得惊奇。但是为什么要有一个将数字转成二进制数,然后对二进制数的各位进行逻辑操作的这么一类操作符呢?首先是CPU提供了相应的指令,并且其还有着下面这个非常有意义的应用。
考虑一十字路口,每个路口有三盏红绿灯,分别指明能否左转、右转及直行。共有12盏,现在要为它编写一个控制程序,不管这程序的功能怎样,首先需要将红绿灯的状态转化为数字,因为电脑只知道数字。所以用3个数字分别表示某路口的三盏红绿灯,因此每个红绿灯的状态由一个数字来表示,假设红灯为0,绿灯为1(不考虑黄灯或其他情况)。
后来忽然发现,其实也可以用一个数字表示一个路口的三盏红绿灯状态,如用110表示左转绿灯、直行绿灯而右转红灯。上面的110是一个十进制数字,它的每一位实际都可以为0~9十个数字,但是这里只应用到了两个:0和1,感觉很浪费。故选择二进制数来表示,还是110,但是是二进制数了,转成十进制数为6,即使当为111时转成十进制数也只是7,比前面的110这个十进制数小多了,节约了……??什么??
我们在纸上写数字235425234一定比写134这个数字要更多地占用纸张(假设字都一样大)。因此记录一个大的数比记录一个小的数要花费更多的资源。简直荒谬!不管是100还是1000,都只是一个数字,为什么记录大的数字就更费资源?因为电脑并不是数字计算机,而是电子计算机,它是基于状态而不是基于数字的,这在下篇会详细说明。电脑必须使用某种表示方式来代表一个数字,而那个表示方式和二进制很像,但并不是二进制数,故出现记录大的数较小的数更耗资源,这也就是为什么上面整型数要分什么长整型短整型的原因了。
下面继续上面的思考。使用了110这个二进制数来表示三盏红绿灯的状态,那么现在要知道110这个数字代表左转红绿灯的什么状态。以数字的第三位表示左转,不过电脑并不知道这个,因此如下:110&100。这个表达式的返回值是100,非零,逻辑真。假设某路口的状态为010,则同样的010&100,返回值为0,逻辑假。因此使用“&”操作符可以将二进制数中的某一位或几位的状态提取出来。所以我们要了解一个数字代表的红绿灯状态中的左转红绿灯是否绿灯时,只需让它和100相与即可。
现在要保持其他红绿灯的状态不变,仅仅使左转红绿灯为绿灯,如当前状态为010,为了使左转红绿灯为绿灯,值应该为110,这可以通过010|100做到。如果当前状态是001,则001|100为101,正确——直行和右转的红绿灯状态均没有发生变化。因此使用“|”操作符可以给一个二进制数中的某一位或几位设置状态,但只能设置为1,如果想设置为0,如101,要关掉左转的绿灯,则101&~100,返回值为001。
上面一直提到的路口红绿灯的状态实际编写时可以使用一个变量来表示,而上面的100也可以用一个标识符来表示,如state&TS_LEFT,就可以表示检查变量state所表示的状态中的左转红绿灯的状态。
上面的这种方法被大量地运用,如创建一个窗口,一个窗口可能有二三十个风格,则通过上面的方法,就可以只用一个32位长的二进制数字就表示了窗口的风格,而不用去弄二三十个数字来分别代表每种风格是否具有。
本用C++实现简单的文件I/O操作
文件 I/O 在C++中比烤蛋糕简单多了。 在这篇文章里,我会详细解释ASCII和二进制文件的输入输出的每个细节,值得注意的是,所有这些都是用C++完成的。
一、ASCII 输出
为了使用下面的方法, 你必须包含头文件<fstream.h>(译者注:在标准C++中,已经使用<fstream>取代< fstream.h>,所有的C++标准头文件都是无后缀的。)。这是 <iostream.h>的一个扩展集, 提供有缓冲的文件输入输出操作. 事实上, <iostream.h> 已经被<fstream.h>包含了, 所以你不必包含所有这两个文件, 如果你想显式包含他们,那随便你。我们从文件操作类的设计开始, 我会讲解如何进行ASCII I/O操作。如果你猜是"fstream," 恭喜你答对了! 但这篇文章介绍的方法,我们分别使用"ifstream"?和 "ofstream" 来作输入输出。
如果你用过标准控制台流"cin"?和 "cout," 那现在的事情对你来说很简单。 我们现在开始讲输出部分,首先声明一个类对象。ofstream fout;
这就可以了,不过你要打开一个文件的话, 必须像这样调用ofstream::open()。
fout.open("output.txt");
你也可以把文件名作为构造参数来打开一个文件.
ofstream fout("output.txt");
这是我们使用的方法, 因为这样创建和打开一个文件看起来更简单. 顺便说一句, 如果你要打开的文件不存在,它会为你创建一个, 所以不用担心文件创建的问题. 现在就输出到文件,看起来和"cout"的操作很像。 对不了解控制台输出"cout"的人, 这里有个例子。
int num = 150;
char name[] = "John Doe";
fout << "Here is a number: " << num << "\n";
fout << "Now here is a string: " << name << "\n";
现在保存文件,你必须关闭文件,或者回写文件缓冲. 文件关闭之后就不能再操作了, 所以只有在你不再操作这个文件的时候才调用它,它会自动保存文件。 回写缓冲区会在保持文件打开的情况下保存文件, 所以只要有必要就使用它。回写看起来像另一次输出, 然后调用方法关闭。像这样:
fout << flush; fout.close();
现在你用文本编辑器打开文件,内容看起来是这样:
Here is a number: 150 Now here is a string: John Doe
很简单吧! 现在继续文件输入, 需要一点技巧, 所以先确认你已经明白了流操作,对 "<<" 和">>" 比较熟悉了, 因为你接下来还要用到他们。继续…
二、ASCII 输入
输入和"cin" 流很像. 和刚刚讨论的输出流很像, 但你要考虑几件事情。在我们开始复杂的内容之前, 先看一个文本:
12 GameDev 15.45 L This is really awesome!
为了打开这个文件,你必须创建一个in-stream对象,?像这样。
ifstream fin("input.txt");
现在读入前四行. 你还记得怎么用"<<" 操作符往流里插入变量和符号吧?好,?在 "<<" (插入)?操作符之后,是">>" (提取) 操作符. 使用方法是一样的. 看这个代码片段.
int number;
float real;
char letter, word[8];
fin >> number; fin >> word; fin >> real; fin >> letter;
也可以把这四行读取文件的代码写为更简单的一行。
fin >> number >> word >> real >> letter;
它是如何运作的呢? 文件的每个空白之后, ">>" 操作符会停止读取内容, 直到遇到另一个>>操作符. 因为我们读取的每一行都被换行符分割开(是空白字符), ">>" 操作符只把这一行的内容读入变量。这就是这个代码也能正常工作的原因。但是,可别忘了文件的最后一行。
This is really awesome!
如果你想把整行读入一个char数组, 我们没办法用">>"?操作符,因为每个单词之间的空格(空白字符)会中止文件的读取。为了验证:
char sentence[101]; fin >> sentence;
我们想包含整个句子, "This is really awesome!" 但是因为空白, 现在它只包含了"This". 很明显, 肯定有读取整行的方法, 它就是getline()。这就是我们要做的。
fin.getline(sentence, 100);
这是函数参数. 第一个参数显然是用来接受的char数组. 第二个参数是在遇到换行符之前,数组允许接受的最大元素数量. 现在我们得到了想要的结果:“This is really awesome!”。
你应该已经知道如何读取和写入ASCII文件了。但我们还不能罢休,因为二进制文件还在等着我们。
三、二进制 输入输出
二进制文件会复杂一点, 但还是很简单的。首先你要注意我们不再使用插入和提取操作符(译者注:<< 和 >> 操作符). 你可以这么做,但它不会用二进制方式读写。你必须使用read() 和write() 方法读取和写入二进制文件. 创建一个二进制文件, 看下一行。
ofstream fout("file.dat", ios::binary);
这会以二进制方式打开文件, 而不是默认的ASCII模式。首先从写入文件开始。函数write() 有两个参数。 第一个是指向对象的char类型的指针, 第二个是对象的大小(译者注:字节数)。 为了说明,看例子。
int number = 30; fout.write((char *)(&number), sizeof(number));
第一个参数写做"(char *)(&number)". 这是把一个整型变量转为char *指针。如果你不理解,可以立刻翻阅C++的书籍,如果有必要的话。第二个参数写作"sizeof(number)". sizeof() 返回对象大小的字节数. 就是这样!
二进制文件最好的地方是可以在一行把一个结构写入文件。 如果说,你的结构有12个不同的成员。 用ASCII?文件,你不得不每次一条的写入所有成员。 但二进制文件替你做好了。 看这个。
struct OBJECT { int number; char letter; } obj;
obj.number = 15;
obj.letter = ‘M’;
fout.write((char *)(&obj), sizeof(obj));
这样就写入了整个结构! 接下来是输入. 输入也很简单,因为read()?函数的参数和 write()是完全一样的, 使用方法也相同。
ifstream fin("file.dat", ios::binary); fin.read((char *)(&obj), sizeof(obj));
我不多解释用法, 因为它和write()是完全相同的。二进制文件比ASCII文件简单, 但有个缺点是无法用文本编辑器编辑。 接着, 我解释一下ifstream 和ofstream 对象的其他一些方法作为结束.
四、更多方法
我已经解释了ASCII文件和二进制文件, 这里是一些没有提及的底层方法。
检查文件
你已经学会了open() 和close() 方法, 不过这里还有其它你可能用到的方法。
方法good() 返回一个布尔值,表示文件打开是否正确。
类似的,bad() 返回一个布尔值表示文件打开是否错误。 如果出错,就不要继续进一步的操作了。
最后一个检查的方法是fail(), 和bad()有点相似, 但没那么严重。
读文件
方法get() 每次返回一个字符。
方法ignore(int,char) 跳过一定数量的某个字符, 但你必须传给它两个参数。第一个是需要跳过的字符数。 第二个是一个字符, 当遇到的时候就会停止。例子,
fin.ignore(100, ‘\n’);
会跳过100个字符,或者不足100的时候,跳过所有之前的字符,包括 ‘\n’。
方法peek() 返回文件中的下一个字符, 但并不实际读取它。所以如果你用peek() 查看下一个字符, 用get() 在peek()之后读取,会得到同一个字符, 然后移动文件计数器。
方法putback(char) 输入字符, 一次一个, 到流中。我没有见到过它的使用,但这个函数确实存在。
写文件
只有一个你可能会关注的方法.?那就是 put(char), 它每次向输出流中写入一个字符。
打开文件
当我们用这样的语法打开二进制文件:
ofstream fout("file.dat", ios::binary);
"ios::binary"是你提供的打开选项的额外标志. 默认的, 文件以ASCII方式打开, 不存在则创建, 存在就覆盖. 这里有些额外的标志用来改变选项。
ios::app 添加到文件尾
ios::ate 把文件标志放在末尾而非起始。
ios::trunc 默认. 截断并覆写文件。
ios::nocreate 文件不存在也不创建。
ios::noreplace 文件存在则失败。
文件状态
我用过的唯一一个状态函数是eof(), 它返回是否标志已经到了文件末尾。 我主要用在循环中。例如, 这个代码断统计小写‘e’ 在文件中出现的次数。
ifstream fin("file.txt");
char ch; int counter;
while (!fin.eof()) {
ch = fin.get();
if (ch == ‘e’) counter++;
}
fin.close();
我从未用过这里没有提到的其他方法。 还有很多方法,但是他们很少被使用。参考C++书籍或者文件流的帮助文档来了解其他的方法。
结论
你应该已经掌握了如何使用ASCII文件和二进制文件。有很多方法可以帮你实现输入输出,尽管很少有人使用他们。我知道很多人不熟悉文件I/O操作,我希望这篇文章对你有所帮助。 每个人都应该知道. 文件I/O还有很多显而易见的方法,?例如包含文件 <stdio.h>. 我更喜欢用流是因为他们更简单。 祝所有读了这篇文章的人好运, 也许以后我还会为你们写些东西。
C++操作符重载的变态用途之子类转换
如果类的成员变量是特定类和自定义结构,使用该类名或结构作为操作符进行重载。(当然是基本类型也可以,不过实用性不强,只会降低代码可读性。)
如下,一个CPerson,强行转换为hand,也可以使用。
类似于现实,我们只会对某个实物的具体特征表示强烈的兴趣,也就是特征聚焦的意思。如HR部门只会关注一个应聘者的skill。
当然在实际用途中,过度使用这种子类转换,只会降低代码可读性。
另外如类中有多个同类型的成员,这样的转换让人莫名其妙。
实例代码:
// Person.h: interface for the CPerson class.
//
//////////////////////////////////////////////////////////////////////
#if !defined(AFX_PERSON_H__A825C71F_CB10_4997_8F9C_DBE792C5C387__INCLUDED_)
#define AFX_PERSON_H__A825C71F_CB10_4997_8F9C_DBE792C5C387__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
typedef struct tag_hand
{
bool bSix;
bool bLefty;
} hand;
class CSkill
{
public:
CSkill():strDesc(NULL){}
virtual ~CSkill(){}
public:
char *strDesc;
};
class CPerson
{
public:
CPerson();
virtual ~CPerson();
hand m_hand;
CSkill m_skill;
operator hand() const;
operator CSkill() const;
static void Test();
};
#endif // !defined(AFX_PERSON_H__A825C71F_CB10_4997_8F9C_DBE792C5C387__INCLUDED_)
// Person.cpp: implementation of the CPerson class.
//
//////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "Person.h"
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
CPerson::CPerson()
{}
CPerson::~CPerson()
{}
CPerson::operator hand() const
{
return m_hand;
}
CPerson::operator CSkill() const
{
return m_skill;
}
void CPerson::Test()
{
CPerson person;
person.m_hand.bSix = false;
person.m_hand.bLefty = true;
person.m_skill.strDesc = new char[1024];
strcpy( person.m_skill.strDesc, "Good at programming..." );
printf( "%d, %d\n", ((hand)person).bSix, ((hand)person).bLefty );
printf( "%s\n", ((CSkill)person).strDesc );
delete[] person.m_skill.strDesc;
return;
}
int main(int argc, char* argv[])
{
CPerson::Test();
return 0;
}
输出:
0, 1
Good at programming...
C/C++中的整型常识
很多人对C/C++中的整型不太了解,导致代码移植的时候出现问题,本人在此总结一下,若有描述错误,请务必指出,谢谢!
a. C/C++对整型长度的规定是为了执行效率,将int定义为机器字长可以取得最大的执行速度;
b. C/C++中整型包括:int, char 和 enum, C++中还包含bool类型,C99中bool是一个宏,实际为_Bool;
c. C 和 C++ 对 enum 的规定有所不同,这里不描述;
d. 修饰整型正负的有 signed 和 unsigned,对于 int 默认为 signed;
e. 修饰 int 大小的有 short 和 long, 部分编译器还扩展了一些更长的整型,比如 long long 和 __int64, C99中增加了long long和unsigned long long;
f. int 的长度 与 机器字长相同, 16位的编译器上int长16位,32位的编译器上int长32位;
g. short int 的长度 小于等于 int 的长度,注意她们可能长度相等,这取决于编译器;
h. long int 的长度 大于等于 int 的长度,注意她们可能长度相等,这取决于编译器;
i. char 的长度应当可以包容得下一个字符,大部分系统中就是一个字节,而有的系统中可能是4个字节,因为这些系统中一个字符需要四个字节来描述;
j. char 的正负取决于编译器,而编译器的决定取决于操作系统,在不同的编译器中char可能等同于signed char,也可能等同于unsigned char;
总结:
a. 出于效率考虑,应该尽量使用int和unsigned int;
b. 当需要指定容量的整型时,不应该直接使用short、int、long等,因为在不同的编译器上她们的容量不相同。此时应该定义她们相应的宏或类型,比如在VC++6.0中,可以如下定义:
typedef unsigned char UBYTE;
typedef signed char SBYTE;
typedef unsigned short int UWORD;
typedef signed short int SWORD;
typedef unsigned int UDWORD;
typedef signed int SDWORD;
typedef unsigned __int64 UQWORD;
typedef signed __int64 SQWORD;
然后在代码中使用 UBYTE、SBYTE、UWORD 等,这样当代码移植的时候只需要修改相应的类型即可。
定义自己的类型虽然在代码移植的时候只需要修改一处即可,但仍然属于源代码级别的修改,所以 C++ 2.0 中将这些类型定义在模板中,可以做到代码移植时无需修改代码。
c. 在定义char时,一定要加上 signed 或 unsigned,因为她的正负在不同的编译器上并不相同。
d. 不要想当然的以为char是1字节长,因为她的长度在不同的编译器上并不相同。
从C++到.NET 揭开多态的面纱
多态是面向对象理论中的重要概念之一,从而也成为现代程序设计语言的一个主要特性,从应用角度来说,多态是构建高灵活性低耦合度的现代应用程序架构所不可忽缺的能力。从概念的角度来说,多态使得程序员可以不必关心某个对象的具体类型,就可以使用这个对象的“某一部分”功能。这个“某一部分”功能可以用基类来呈现,也可以用接口来呈现。后者显得更为重要——接口是使程序具有可扩展性的重要特性,而接口的实现依赖于语言对多态的实现,或者干脆就象征着语言对多态的实现。
本文并不大算赘述多态的应用,因为其应用实在俯拾皆是,其概念理论也早已完善。这里,我们打算从实现的角度来看一看一门语言在其多态特性的背后做了些什么——知其所以然,使用时方能游刃有余。
或许你在学习一门语言的时候,曾经对多态的特性很迷惑,虽然教科书上所讲的非常简单,也非常明了——正如它的原本理念一样,但是你也想知道语言(编译器)在背后都干了些什么,为什么一个派生类对象就可以被当作其基类对象来使用?用指向派生类对象的基类指针调用虚函数时凭什么能够精确的到达正确的函数?类的内部是如何布局的?
我们这样考虑:假设语言不支持多态,而我们又必须实现多态,我们可以怎么做?
多态的雏形:
class B
{
public:
int flag; //为表示简洁,0代表基类,1代表派生类
void f(){cout<<”in B::f()”;} //非虚函数
};
class D:public B
{
public:
void f(){cout<<”in D::f()”;} //非虚函数
};
void call_virtual(B* pb)
{
if(pb->flag==0) //如果是基类,则直接调用f
pb->f(); //调用的是基类的f
else //如果是派生类,则强制转化为派生类指针再调用f
(D*)pb->f(); //调用的是派生类的f
}
这样,可以正好符合“根据具体的对象类型调用相应的函数”的理念。但是这个原始方案有一些缺点:;例如,分发“虚函数”的代码要自己书写,不够优雅,不具有可扩展性(当继承体系扩大时,这堆代码将变得臃肿无比),不具有封闭性(如果加入了一个新的派生类,则“虚函数”调用的代码必须作改动,然而如果恰巧这个调用是无法改动的(例如,库函数),则意味着,一个用户加入的派生类将无法兼容于那个库函数)等等。结果就是——这个方案不具有通用性。
但是,这个方案能够说明一些本质性的问题:flag数据成员用于标识对象所属的具体类型,从而调用者可以根据它来确定到底调用哪个函数。但是,可不可以不必“知道”对象的具体类型就能够调用正确的函数呢?可以,改进的方案如下:
class B
{
public:
void (*f)(); //函数指针,派生类对象可以通过给它重新赋值来改变对象的行为
};
class D:public B
{};
void call_virtual(B* pb)
{
(*(pb->f))(); //间接调用f所指的函数
}
void B_Mem()
{
cout<<”I am B”;
}
void D_Mem()
{
cout<<”I am D”;
}
int main()
{
B b;
b.f=&B_Mem; //B_Mem代表B的“虚函数”
D d;
d.f=&D_Mem; //以D_Mem来覆盖(override)B的虚函数
call_virtual(&b); //输出“I am B”
call_virtual(&d); //输出“I am D”
}
在这个改进的例子中,派生类对象可以通过修改函数指针f的指向,从而获得特定的行为,这里重要的是,call_virtual函数不再需要通过丑陋的if-else语句来判断对象的具体类型,而只是简单的通过一个指针来调用“虚函数”——这时候,如果派生类需要改变具体的行为,则可以将相应的函数指针指向它自己的函数即可,这招“偷梁换柱”通过增加一个间接层的办法“神不知鬼不觉”地将“虚函数”替换(Override)掉了。
然而,这招仍然还有缺点——要用户手动实现,可扩展性差,透明性差等等。然而,它的思想已经接近现代编译器对多态机制的实现手法了。
通过将上面的例子中的函数指针扩展为一个隐含的指针数组——虚函数表(vtbl)——C++拥有了我们现在所看到的多态能力。在虚函数表中,每一个虚函数指针占有一个表项,如果派生类覆盖(override)了相应的虚函数,则对应表项就改成指向派生类的那个虚函数的——这些工作由编译器完成——从而,如上例所示,用户不必知晓对象的确切类型,就能够触发其特定的行为(也就是说,调用“取决于对象具体类型”的成员函数),虚函数表对用户是完全透明的,用户只需要使用一个virtual关键字就能够轻松拥有强大的多态能力。
如果一个C++类中有虚函数,则该类将会拥有一个虚函数表(vtbl),并且,该类的对象中(一般在头部)有一个隐含的指向虚函数表的指针(vptr)。
现在假设有如下代码:
void f(B* pb)
{
pb->f1();
}
则编译器为该函数生成的代码如下(以伪代码表示,以示明了):
void f(B* pb)
{
DWORD* __vptr=((DWORD*)pb)[0]; //获得虚函数表指针
void (B::*midd_pf)()=__vptr[offsetof_virtual_pf1];
//从表中获得相应虚函数指针
(pb->*midd_pf)(); //调用虚函数
}
这样一来,如果pb指向的是D对象,则获得的是指向D::f1的函数指针(参考上面的第二幅图),如果pb确实指向B对象,根据B对象内的vptr所指的虚函数表,获得的是指向B::f1的函数指针。
现在,关于C++的多态机制基本已经明了。剩下的就是多重继承下的虚函数表格局,大同小异,就不多说了。只不过,其中还是有一些微妙的细节的,可以参见《Inside C++ Object Model》(Lippman著)(中文名《深入C++对象模型》——侯捷译)。
关于C++虚函数调用机制还有一个细节——在构造函数中调用虚函数要千万小心,因为“在构造函数中”意味着“对象还没有构造完毕”,这时候虚函数调用机制很可能还没有启动,例如:
class B
{
B(){this->vf();} //调用B::vf
virtual void vf(){cout<<”in B::vf()\n”;
};
现在,不管B身为哪个类的基类,B的构造函数中调用的都是B::vf。细心的读者会发现:这是由于对象构造顺序的关系——C++明确规定,对象的“大厦”是“自底向上”构建的,也就是说,从最底层的基类开始构造,所以,在B中调用this->vf时,虽然this所指的对象确实(即将)是派生类对象,但是派生类对象的构建行为还没有开始,所以这次调用不可能跑到派生类的vf函数去,就好像第二层楼还没有建好,一层楼的人是无法跑到二楼去的一样。
说得更深一些,虚函数的调用是要经过虚函数指针和虚函数表来间接推导的,在B的构造函数中,编译器会插入一些代码,将对象头部的vptr设置为指向B的虚函数表的指针,于是this->vf的推导使用的是B的虚函数表,当然只能跑到B的vf那儿去。而后来,当B构建完毕,轮到派生类对象部分构造时,派生类的构造函数会将对象头部的vptr改成指向派生类的虚函数表的指针,这时候虚函数调用机制才算是Enable了,以后的this->vf将使用派生类虚函数表来推导,从而到达正确的函数。
.NET 对象模型
C++对象模型与.NET(或Java)有个主要的区别——C++支持多重继承,不支持接口,而.NET(或Java)支持接口,不支持多重继承。
而.NET的虚函数调用机制与C++也比较相似,只不过由于接口和JIT(即时编译)的介入而有一些不同。
在.NET中,每一个类都有一个对应的函数指针表(事实上,这个“表”是个数据结构,里面还有其它信息),与C++不同的是,该类的每个函数(不管是不是虚函数)都在其中对应一个表项。这是由于JIT(即时编译)的需要——对每个函数的调用都是间接的,都会经过该表推导一次,获得函数代码的地址。注意,第一次调用的时候,函数代码还是中间代码(.NET的中间语言MISL的代码),所以将会跳至即时编译器,编译这些代码并放到内存中,然后将表中的对应表项指向编译后的native code,以后的每次调用都会直接跳到编译后的代码。
以上只是想让你对.NET的“虚函数表”有个大体的认识。下面就来详细剖析。
如果没有接口,.NET的虚函数调用机制将是很单纯的——几乎与C++一样。只不过,接口加入以后就不同了——可以将对象引用转化为接口引用,然后再调用接口中的虚函数。所以,势必要对“虚函数表”作某种改动,例如,对于下面的继承结构:
public interface IFirst
{
void f1();
void f2();
}
public interface ISecond
{
void s1();
}
public class C:IFirst,Isecond
{
public override void f1(){}
public override void f2(){}
public override void s1(){}
public virtual void c1(){}
}
类型C的内存布局大体是这样的(由于.NET是单根的继承结构,每个类都隐式的继承自Object,所以,类型C的“虚函数表”中包含Object的所有成员函数)
ObjRef指向一个对象,在对象顶部(除了用于同步的sync#块之外)是hType(可以看成对应于C++对象顶部的虚函数表指针),它所指的结构(CORINFO_CLASS_STRUCT,可以暂时将它看成虚函数表,尽管其中包含的信息不仅仅是虚函数指针)包含在C++中相当于虚函数表的部分,以及用于对象的运行时识别的信息。不同的是,在基于接口的.NET继承风格中,对接口的虚函数的分派是基于一个IOT(Interface Offset Table,即接口偏移表),图中的pIOT就是指向这样一个表,其中每一项都是一个偏移量,反指向该接口中的虚函数指针数组在CORINFO_CLASS_STRUCT中的位置。
这样,当基于接口的引用调用虚函数时,其背后的机制是:先根据接口引用取得该类所对应的CORINFO_CLASS_STRUCT结构的地址,然后在pIOT所指的接口偏移表中索引相应的虚函数指针数组的偏移量,最后经过指针间接调用虚函数。 可以看出,基于接口引用调用虚函数时要经过两个间接层,第一,在IOT中索引对应接口的虚函数指针数组的偏移量,第二,在虚函数指针数组中索引相应的虚函数指针,最后才是调用。但是,当基于对象引用调用虚函数时,只要经过一个间接层——就像在C++中一样——直接在虚函数表中索引对应虚函数指针,接着调用。
关于基于接口的引用调用虚函数,还有一个细节就是,IOT里为每一个接口都准备了一个表项(包括该类并没有实现的接口),原因是效率——.NET需要每个接口在IOT里都有一个固定的(或者说,编译期确定的)偏移量,这样,在为虚函数调用生成代码的时候才能够通过这个固定的偏移去查找某个接口的虚函数指针数组的所在。 另一方面,如果某个类的IOT仅仅包含它实现的接口,则经由接口引用去调用虚函数时,必须先知道该接口在IOT中的相应偏移,而这一信息必须通过运行期的动态查询才能够知道(因为编译器在手头只有一个接口引用的情况下不可能知道它指向的是哪个类对象,从而也就不知道该类到底实现了哪些接口,所以要求助于运行期的动态查询,而在前面所说的方式(也就是.NET所用的方式)下,编译器不用知道接口引用到底指向哪个类对象,因为在每个类的CORINFO_CLASS_STRUCT中的固定位置都有一个pIOT,指向一个IOT,其中每个接口都对应一个固定的(编译器知道的)表项)——显然,在每次调用虚函数之前都进行一次动态查询是不可容忍的效率损伤,所以.NET宁可让IOT多一些表项,以空间换时间。
或许你认为这过于复杂,但是这是必须的,.NET中的基于接口的继承对应于C++中的多重继承,后者的实现也有类似的复杂性——或许更复杂一些。
最后,要说明的是,本文对于一个纯粹的实用者或许显得多余,但是对于想把一门语言使用得更好的人却是有用的。知其然而知其所以然,才能够游刃有余。而其实现机理在实际运用中能起到抛砖引玉的作用也未可知。
C++ 中重载 + 操作符的正确方法
用户定义的类型,如:字符串,日期,复数,联合体以及文件常常重载二元 + 操作符以实现对象的连接,附加或合并机制。但是要正确实现 + 操作符会给设计,实现和性能带来一定的挑战。本文将概要性地介绍如何选择正确的策略来为用户定义类型重载这个操作符。
考虑如下的表达式: int x=4+2;
内建的 + 操作符有两个类型相同的操作数,相加并返回右值 6,然后被赋值给 x。我们可以断定内建的 + 是一个二元的,对称的,可交换的操作符。它产生的结果的类型与其操作数类型相同。按照这个规测,当你为某个用户定义类型重载操作符时,也应该遵循相应内建操作符的特征。
为用户定义类型重载 + 操作符是很常见的编程任务。尽管 C++ 提供了几种实现方法,但是它们容易使人产生设计上的误解,这种误解常常影响代码的正确性,性能以及与标准库组件之间的兼容性。
下面我们就来分析内建操作符的特征并尝试模仿其相应的重载机制。
第一步:在成员函数和非成员函数之间选择
你可以用类成员函数的方式实现二元操作符如:+、- 以及 ==,例如:
class String
{
public:
bool operator==(const String & s); // 比较 *this 和 s
};
这个方法是有问题的。相对于其内建的操作符来说,重载的操作符在这里不具有对称性;它的两个参数一个类型为:const String * const(这个参数是隐含的),另一个类型为:const String &。因此,一些 STL 算法和容器将无法正确处理这样的对象。
另外一个可选方法是把重载操作符 + 定义为一个外部(extern)函数,该函数带两个类型相同的参数:
String operator + (const String & s1, const String s2);
这样一来,类 String 必须将该重载操作符声明为友元:
class String
{
public:
friend String operator+(const String& s1,const String&s2);
};
第二步:返回值的两难选择
如前所述,内建操作符 + 返回右值,其类型与操作数相同。但是在调用者堆栈里返回一个对象效率很低,处理大型对象时尤其如此。那么能不能返回一个指针或引用呢?答案是不行。因为返回指针破坏参数类型与返回值类型应该相同的规则。更糟的是,链接多个表达式将成为不可能:
String s1,s2,s3;
String res;
res=s1+s2+s3; // 不可能用 String* 作为返回值
虽然有一个办法可以定义额外的 + 操作符重载版本,但这个办法是我们不希望用的,因为返回的指针必须指向动态分配的对象。这样的话,如果调用者释放(delete)返回的指针失败,那么将导致内存泄漏。显然,返回 String* 不是一个好主意。
那么返回 String& 好不好呢?返回的引用必须一定要是一个有效的 String。它避免了使用动态对象分配,该方法返回的是一个本地静态对象的引用。静态对象确实解决了内存泄漏问题,但这个方法的可行性仍然值得怀疑。在一个多线程应用中,两个线程可能会并发调用 + 操作符,因此造成 String 对象的混乱。而且,因为静态对象总是保留其调用前的状态,所以有必要针对每次 + 操作符的调用都清除该静态 String 对象。由此看来,在堆栈上返回结果仍然是最安全和最简单的解决方案。
C++中用函数模板实现和优化抽象操作
本文介绍函数模板的概念、用途以及如何创建函数模板和函数模板的使用方法......
在创建完成抽象操作的函数时,如:拷贝,反转和排序,你必须定义多个版本以便能处理每一种数据类型。以 max() 函数为例,它返回两个参数中的较大者:
double max(double first, double second);
complex max(complex first, complex second);
date max(date first, date second);
//..该函数的其它版本
尽管这个函数针对不同的数据类型其实现都是一样的,但程序员必须为每一种数据类型定义一个单独的版本:
double max(double first, double second)
{
return first>second? first : second;
}
complex max(complex first, complex second)
{
return first>second? first : second;
}
date max(date first, date second)
{
return first>second? first : second;
}
这样不但重复劳动,容易出错,而且还带来很大的维护和调试工作量。更糟的是,即使你在程序中不使用某个版本,其代码仍然增加可执行文件的大小,大多数编译器将不会从可执行文件中删除未引用的函数。
用普通函数来实现抽象操作会迫使你定义多个函数实例,从而招致不小的维护工作和调试开销。解决办法是使用函数模板代替普通函数。
使用函数模板
函数模板解决了上述所有的问题。类型无关并且只在需要时自动实例化。本文下面将展示如何定义函数模板以便抽象通用操作,示范其使用方法并讨论优化技术。
第一步:定义
函数模板的声明是在关键字 template 后跟随一个或多个模板在尖括弧内的参数和原型。与普通函数相对,它通常是在一个转换单元里声明,而在另一个单元中定义,你可以在某个头文件中定义模板。例如:
// file max.h
#ifndef MAX_INCLUDED
#define MAX_INCLUDED
template <class T> T max(T t1, T t2)
{
return (t1 > t2) ? t1 : t2;
}
#endif
<class T> 定义 T 作为模板参数,或者是占位符,当实例化 max()时,它将替代具体的数据类型。max 是函数名,t1和t2是其参数,返回值的类型为 T。你可以像使用普通的函数那样使用这个 max()。编译器按照所使用的数据类型自动产生相应的模板特化,或者说是实例:
int n=10,m=16;
int highest = max(n,m); // 产生 int 版本
std::complex<double> c1, c2;
//.. 给 c1,c2 赋值
std::complex<double> higher=max(c1,c2); // complex 版本
第二步:改进设计
上述的 max() 的实现还有些土气——参数t1和t2是用值来传递的。对于像 int,float 这样的内建数据类型来说不是什么问题。但是,对于像std::complex 和 std::sting这样的用户定义的数据类型来说,通过引用来传递参数会更有效。此外,因为 max() 会认为其参数是不会被改变的,我们应该将 t1和t2声明为 const (常量)。下面是 max() 的改进版本:
template <class T> T max(const T& t1, const T& t2)
{
return (t1 > t2) ? t1 : t2;
}
额外的性能问题
很幸运,标准模板库或 STL 已经在 <algorithm> 里定义了一个叫 std::max()的算法。因此,你不必重新发明。让我们考虑更加现实的例子,即字节排序。众所周知,TCP/IP 协议在传输多字节值时,要求使用 big endian 字节次序。因此,big endian 字节次序也被称为网络字节次序(network byte order)。如果目的主机使用 little endian 次序,必须将所有过来的所字节值转换成 little endian 次序。同样,在通过 TCP/IP 传输多字节值之前,主机必须将它们转换成网络字节次序。你的 socket 库声明四个函数,它们负责主机字节次序和网络字节次序之间的转换:
unsigned int htonl (unsigned int hostlong);
unsigned short htons (unsigned short hostshort);
unsigned int ntohl (unsigned int netlong);
unsigned short ntohs (unsigned short netshort);
这些函数实现相同的操作:反转多字节值的字节。其唯一的差别是方向性以及参数的大小。非常适合模板化。使用一个模板函数来替代这四个函数,我们可以定义一个聪明的模板,它会处理所有这四种情况以及更多种情形:
template <class T> T byte_reverse(T val);
为了确定 T 实际的类型,我们使用 sizeof 操作符。此外,我们还使用 STL 的 std::reverse 算法来反转值的字节:
template <class T> T byte_reverse(T val)
{
// 将 val 作为字节流
unsigned char *p=reinterpret_cast<unsigned char*> (&val);
std::reverse(p, p+sizeof(val));
return val;
}
使用方法
byte_reverse() 模板处理完全适用于所有情况。而且,它还可以不必修改任何代码而灵活地应用到其它原本(例如:64 位和128位)不支持的类型:
int main()
{
int n=1;
short k=1;
__int64 j=2, i;
int m=byte_reverse(n);// reverse int
int z=byte_reverse(k);// reverse short
k=byte_reverse(k); // un-reverse k
i=byte_reverse(j); // reverse __int64
}
注:模板使用不当会影响.exe 文件的大小,也就是常见的代码浮肿问题
C++中用vectors改进内存的再分配
摘要:本文描述的是一种很常见的情况:当你在某个缓存中存储数据时,常常需要在运行时调整该缓存的大小,以便能容纳更多的数据。本文将讨论如何使用 STL 的 vector 进行内存的再分配。
这里描述的是一种很常见的情况:当你在某个缓存中存储数据时,常常需要在运行时调整该缓存的大小,以便能容纳更多的数据。传统的内存再分配技术非常繁琐,而且容易出错:在 C 语言中,一般都是每次在需要扩充缓存的时候调用 realloc()。在 C++ 中情况更糟,你甚至无法在函数中为 new 操作分配的数组重新申请内存。你不仅要自己做分配处理,而且还必须把原来缓存中的数据拷贝到新的目的缓存,然后释放先前数组的缓存。本文将针对这个问题提供一个安全、简易并且是自动化的 C++ 内存再分配技术——即使用 STL 的 vector。
用 STL vector 对象取代内建的数组来保存获取的数据,既安全又简单,并且是自动化的。
进一步的问题分析
在提出解决方案之前,我先给出一个具体的例子来说明 C++ 重新分配内存的弊病和复杂性。假设你有一个编目应用程序,它读取用户输入的 ISBNs,然后将之插入一个数组,直到用户输入 0 为止。如果用户插入的数据多于数组的容量,那么你必须相应地增加它的大小:
#include <iostream>
using namespace std;
int main()
{
int size=2; // 初始化数组大小;在运行时调整。
int *p = new int[size];
int isbn;
for(int n=0; ;++n)
{
cout<< "enter an ISBN; press 0 to stop ";
cin>>isbn;
if (isbn==0)
break;
if (n==size) // 数组是否到达上限?
reallocate(p, size);
p[n]=isbn; // 将元素插入扩容的数组
}
delete [] p; // 不要忘了这一步!
}
注意上述这个向数组插入数据的过程是多么的繁琐。每次反复,循环都要检查缓存是否达到上限。如果是,则程序调用用户定义的函数 reallocate(),该函数实现如下:
#include <algorithm> // for std::copy
int reallocate(int* &p, int& size)
{
size*=2; // double the array''s size with each reallocation
int * temp = new int[size];
std::copy(p, p+(size/2), temp);
delete [] p; // release original, smaller buffer
p=temp; // reassign p to the newly allocated buffer
}
reallocate() 使用 STL std::copy() 算法对缓存进行合理的扩充——每次扩充都放大一倍。这种方法可以避免预先分配过多的内存,从量上减少需要重新分配的内存。这个技术需要得到充分的测试和调试,当初学者实现时尤其如此。此外,reallocate() 并不通用,它只能处理整型数组的情形。对于其它数据类型,它无能为力,你必须定义该函数额外的版本或将它模板化。幸运的是,有一个更巧妙的办法来实现。
创建和优化 vector
每一个 STL 容器都具备一个分配器(allocator),它是一个内建的内存管理器,能自动按需要重新分配容器的存储空间。因此,上面的程序可以得到大大简化,并摆脱 reallocator 函数。
第一步:创建 vector
用 vector 对象取代内建的数组来保存获取的数据。main() 中的循环读取 ISBN,检查它是否为 0,如果不为 0 ,则通过调用 push_back() 成员函数将值插入
vector: #include <iostream>
#include <vector>
using namespace std;
int main()
{
vector <int> vi;
int isbn;
while(true)
{
cout << "enter an ISBN; press 0 to stop ";
cin >> isbn;
if (isbn==0)
break;
vi.push_back(isbn); // insert element into vector
}
}
在 vector 对象构造期间,它先分配一个由其实现定义的默认的缓存大小。一般 vector 分配的数据存储初始空间是 64-256 存储槽(slots)。当 vector 感觉存储空间不够时,它会自动重新分配更多的内存。实际上,只要你愿意,你可以调用 push_back() 任何多次,甚至都不用知道一次又一次的分配是在哪里发生的。
为了存取 vector 元素,使用重载的 [] 操作符。下列循环在屏幕上显示所有 vector 元素:
for (int n=0; n<vi.size(); ++n)
{
cout<<"ISBN: "<<vi[n]<<endl;
}
第二步:优化
在大多数情况下,你应该让 vector 自动管理自己的内存,就像我们在上面程序中所做的那样。但是,在注重时间的任务中,改写默认的分配方案也是很有用的。假设我们预先知道 ISBNs 的数量至少有 2000。那么就可以在对象构造期间指出容量,以便 vector 具有至少 2000 个元素的容量:
vector <int> vi(2000); // 初始容量为 2000 个元素
除此之外,我们还可以调用 resize() 成员函数:
vi.resize(2000);// 建立不小于 2000 个元素的空间
这样,便避免了中间的再分配,从而提高了效率。
深入探讨C++中的引用
摘要:介绍C++引用的基本概念,通过详细的应用分析与说明,对引用进行全面、透彻地阐述。
关键词:引用,const,多态,指针
引用是C++引入的新语言特性,是C++常用的一个重要内容之一,正确、灵活地使用引用,可以使程序简洁、高效。我在工作中发现,许多人使用它仅仅是想当然,在某些微妙的场合,很容易出错,究其原由,大多因为没有搞清本源。故在本篇中我将对引用进行详细讨论,希望对大家更好地理解和使用引用起到抛砖引玉的作用。
引用简介
引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。
引用的声明方法:类型标识符 &引用名=目标变量名;
【例1】:int a; int &ra=a; //定义引用ra,它是变量a的引用,即别名
说明:
(1)&在此不是求地址运算,而是起标识作用。
(2)类型标识符是指目标变量的类型。
(3)声明引用时,必须同时对其进行初始化。
(4)引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。
ra=1; 等价于 a=1;
(5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra与&a相等。
(6)不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。
引用应用
1、引用作为参数
引用的一个重要作用就是作为函数的参数。以前的C语言中函数参数传递是值传递,如果有大块数据作为参数传递的时候,采用的方案往往是指针,因为这样可以避免将整块数据全部压栈,可以提高程序的效率。但是现在(C++中)又增加了一种同样有效率的选择(在某些特殊情况下又是必须的选择),就是引用。
【例2】:
void swap(int &p1, int &p2) //此处函数的形参p1, p2都是引用
{ int p; p=p1; p1=p2; p2=p; }
为在程序中调用该函数,则相应的主调函数的调用点处,直接以变量作为实参进行调用即可,而不需要实参变量有任何的特殊要求。如:对应上面定义的swap函数,相应的主调函数可写为:
main( )
{
int a,b;
cin>>a>>b; //输入a,b两变量的值
swap(a,b); //直接以变量a和b作为实参调用swap函数
cout<<a<< ' ' <<b; //输出结果
}
上述程序运行时,如果输入数据10 20并回车后,则输出结果为20 10。
由【例2】可看出:
(1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。
2、常引用
常引用声明方式:const 类型标识符 &引用名=目标变量名;
用这种方式声明的引用,不能通过引用对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性。
【例3】:
int a ;
const int &ra=a;
ra=1; //错误
a=1; //正确
这不光是让代码更健壮,也有些其它方面的需要。
【例4】:假设有如下函数声明:
string foo( );
void bar(string & s);
那么下面的表达式将是非法的:
bar(foo( ));
bar("hello world");
原因在于foo( )和"hello world"串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。
引用型参数应该在能被定义为const的情况下,尽量定义为const 。
3、引用作为返回值
要以引用返回函数值,则函数定义时要按以下格式:
类型标识符 &函数名(形参列表及类型说明)
{函数体}
说明:
(1)以引用返回函数值,定义函数时需要在函数名前加&
(2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。
【例5】以下程序中定义了一个普通的函数fn1(它用返回值的方法返回函数值),另外一个函数fn2,它以引用的方法返回函数值。
#include <iostream.h>
float temp; //定义全局变量temp
float fn1(float r); //声明函数fn1
float &fn2(float r); //声明函数fn2
float fn1(float r) //定义函数fn1,它以返回值的方法返回函数值
{
temp=(float)(r*r*3.14);
return temp;
}
float &fn2(float r) //定义函数fn2,它以引用方式返回函数值
{
temp=(float)(r*r*3.14);
return temp;
}
void main() //主函数
{
float a=fn1(10.0); //第1种情况,系统生成要返回值的副本(即临时变量)
float &b=fn1(10.0); //第2种情况,可能会出错(不同 C++系统有不同规定)
//不能从被调函数中返回一个临时变量或局部变量的引用
float c=fn2(10.0); //第3种情况,系统不生成返回值的副本
//可以从被调函数中返回一个全局变量的引用
float &d=fn2(10.0); //第4种情况,系统不生成返回值的副本
//可以从被调函数中返回一个全局变量的引用
cout<<a<<c<<d;
}
引用作为返回值,必须遵守以下规则:
(1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
(2)不能返回函数内部new分配的内存的引用。这条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
(3)可以返回类成员的引用,但最好是const。这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
(4)引用与一些操作符的重载:
流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << "hello" << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一个流指针则不能连续使用<<操作符。因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因吧。 赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。
【例6】 测试用返回引用的函数值作为赋值表达式的左值。
#include <iostream.h>
int &put(int n);
int vals[10];
int error=-1;
void main()
{
put(0)=10; //以put(0)函数值作为左值,等价于vals[0]=10;
put(9)=20; //以put(9)函数值作为左值,等价于vals[9]=10;
cout<<vals[0];
cout<<vals[9];
}
int &put(int n)
{
if (n>=0 && n<=9 ) return vals[n];
else { cout<<"subscript error"; return error; }
}
(5)在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一个静态对象引用。根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。
4、引用和多态
引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。
【例7】:
class A;
class B:public A{……};
B b;
A &Ref = b; // 用派生类对象初始化基类对象的引用
Ref 只能用来访问派生类对象中从基类继承下来的成员,是基类引用指向派生类。如果A类中定义有虚函数,并且在B类中重写了这个虚函数,就可以通过Ref产生多态效果。
引用总结
(1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
(2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
(3)引用与指针的区别是,指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
(4)使用引用的时机。流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。