C++实在C的基础上发展而来,C++是兼容C的,包含了C的所有特性,另外增加了新的特性。
C是面向过程的语言,它的侧重点在于算法和数据结构。编写C代码,侧重点在于通过输入数据,设计一定的算法过程,得到输出。
C++是面向对象的语言,它的侧重点在于抽象出对象模型,使这个模型和问题像契合。通过对对象状态的控制,来解决问题。
C++不同于C的部分包含:
class:虽然C语言也有结构体struct,但是它更多的侧重于数据机构,侧重数据的组织。虽然struct在++中也支持class的个各种特性,但是很少用struct去替代class。它们两个一个不同在于class默认成员的访问是private,而struct是public。
template:模板属于泛型编程,泛型编程使得代码独立于特定的数据类型,可以大大减少代码量。
overload:重载是C语言中没有的,在一些代码中经常看到external “C",这是表示以C语言方式编译。因为重载是通过编译时,在函数明后加上函数参数类型来生成函数名实现的,而C语言则不是,所以如果要给C调用,就要加上extern "C"。
除了以上几点,还有其他,例如引用、异常等。
可以将C++分为4个层次:
1、C:C++实在C语言的基础上发展而来的。
2:Object-Oriented C++:这是C++中不同于C的部分,这里主要指面向对象。
3:Template C++:C++中的泛型编程。
4:STL:这是一个标准模板库,它用模板实现了很多容器、迭代器和算法,使用STL往往事半功倍。
这句话也可以说:以编译器代替预处理。
例如,你使用预处理定义了圆周率
#define PI 3.1415926
在预处理时, 所有使用PI的地方都将被替换,之后编译器在编译时从未看到过PI。这时如果遇到错误,报错时给出的是301415926,而不是PI,因为PI从未进入到符号表,这将导致错误难以理解。一个替换的方法是如下定义:
const double PI=3.1415926
当用常量替换#define时,有两点要注意
1:替换字符串时,要定义成常量指针,而不是指向常量的指针,例如定义一个名字
#define name "xiaoming"
替换时定义成
char* const name="xiaoming"
而不是
const char* name="xiaoming"
2:专属于class作用域的常量。专属于class的常量将这个常量限定在class的作用域内,而#define定义的常量没有作用域的限制,一旦在某一处有个宏定义,在其后面都有效(除非#undef)。
class GamePlayer{
static const int NumTurns=5;
int scores[NumTurns];
};
NumTurns只在GamePlayer的作用域内有效。NumTurns会分配内存(虽然它只有一个实例),我们可以获取它的地址。通过枚举enum可以避免内存分配,当然也就无法获取其地址了。
class GamePlayer{
enum{NumTurns=5};
int scores[NumTurns];
};
现在再来看宏定义,加入定义一个比较大小的宏
#define MAX(a,b) ((a)>(b)?(a):(b))
这样调用后a的值为11。
int a=10;
MAX(a++,12);
而这样调用后,a的值为12。这取决于宏的展开。
int a=10; MAX(a++,2);
这样的不确定性是不允许的。
在宏定义中经常见到do{}while{0},至于原因可以看这里。
虽然C++中可以降低对宏的依赖,但是宏定义依然非常必要。在一些库中,尤其涉及到跨平台的库中,经常看到非常多的宏定义。另外使用宏,可以提高程序的可读性,例如
#define long long ll
定义长长整型。
最后看一个有意思的题目:判断下面的输出
#include
#define f(a,b) a##b
#define g(a) #a
#define h(a) g(a)
int main() {
printf("%s\n",h(f(1,2)));
printf("%s\n",g(f(1,2)));
return 0;
}
答案是:
12
f(1,2)
这涉及到宏的展开过程:Macro arguments are completely macro-expanded before they are substituted into a macro body, unless they are stringified or pasted with other tokens.
After substitution, the entire macro body, including the substituted arguments, is scanned again for macros to be expanded.
The result is that the arguments are scanned twice to expand macro calls in them.
使用关键字const修饰的变量不允许被改动。
使用const修饰指针或指针指向的对象时要注意:
const T * point或T const* point表示point指向的对象是常量。
T * const point;表示point指针是常量指针。
STL中的迭代器类似指针,当需要使用一个常量迭代器时(即迭代器不能改动,其指向的元素可以修改,类似T * const point)
const vector
如果需要迭代器指向常量,类似const T* point
vector
const成员函数的目的是确认该函数可以用到const对象上。const成员函数使得1、class接口更加容易理解,确认哪些接口可以修改class成员。2、使操作const对象成为可能。
关于第2点,是因为const对象只能调用const成员函数;但是非const对象既可以调用普通成员函数,也可以调用const成员函数。这是因为this指针可以转换为const this,但是const this不能转换为非const this。
一个函数是不是const是可以被重载的。
关于const成员函数有2个概念:bitwise constness和logical constness
bitwise constness是指const对象的每一个bit都不能被修改,这个说法是正确的。但是包含指针时,有时就比较费解。
class CTextBlock{
public:
char& operator[](std::size_t position)const
{
return pText[position];
}
char * pText;
int length;//指针pText指向内存的长度
};
上面的[]重载是个const成员函数,返回一个引用。这个引用并不属于对象,因为指针pText指向的内存并不在类中定义,所以可以修改[]的返回值,这好像有点不符合逻辑,但这的确是符合bitwise constness,因为并没有修改指针pText。所以,const成员函数并不能实现logical constness。
如果我们修改了指针pText指向的内存,那么length可能会变,const函数不能修改,怎么办?
可以使用mutable关键字。
在一些类中,const成员函数和non-const成员函数功能类似,在这两个函数中都要执行相同的代码,比如一些检查等。
class CTextBlock{
public:
const char& operator[](std::size_t position)const
{
prepare();//准备
return pText[position];
}
char& operator[](std::size_t position)
{
prepare();//准备
return pText[position];
}
void prepare()const;//一些准备动作
char * pText;
int length;
};
这样看起来比较臃肿,可以是用来类型转换,把non-const转换为const,再转换其返回值类型
class CTextBlock{
public:
const char& operator[](std::size_t position)const
{
prepare();//准备
return pText[position];
}
char& operator[](std::size_t position)
{
return const_cast(static_cast(*this)[position]);
}
void prepare()const;//一些准备动作
char * pText;
int length;
};
int x;
在一些语境下会初始化为0,但在另一些语境下可能就不会初始化,例如
class Point{
int x,y;
}
如果使用未初始化的对象,可能会导致不确定的行为。所以,在使用对象之前,一定要将其初始化。
int x=0;
const char* text="A C-style string";
double d;
std::cin>>d;//以读取input stream方式完成初始化
对于内置类型以外的数据类型,则通过构造函数完成初始化。
class Point{
public:
Point(int x_, int y_)
{
x=x_;
y=y_;
}
int x,y;
}
下面才是初始化, 要注意,初始化顺序要和声明顺序一致。
class Point{
public:
Point(int x_, int y_):x(x_),y(y_)
{
}
int x,y;
}
初始化效率往往高于赋值。赋值是先定义变量,在定义的时候可能已经调用了变量的构造函数,之后赋值是调用了赋值操作符。
class FileSystem{
public:
……
std::size_t numDisks()const;
……
};
extern FileSystem tfs;//定义在global作用域
现在一个客户新建一个类来处理文件系统内的目录,会使用定义在global作用域内的tfs
class Directory{
public:
Directory( params )
{
……
std::size_t disks=tfs.numDisks();
……
}
};
Directory tempDir( params);
class FileSystem{
public:
……
std::size_t numDisks()const;
……
};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
class Directory{
public:
Directory( params )
{
……
std::size_t disks=tfs.numDisks();
……
}
};
Directory tempDir()
{
static Directory td;
return td;
}
这样修改之后,客户代码无需修改即可使用。