C++基本知识
最近由于毕业论文的需要,总结了C++的基本概念,帮助自己快速了解C++的常见特性。
1.类定义
point2d.h
#ifndef POINT2D_H
#define POINT2D_H
class Point2D {
public:
Point2D();
Point2D(double x, double y) {
xVal = x;
yVal = y;
}
void setX(double x) {
xVal = x;
}
void setY(double y){
yVal = y;
}
double x() const {
return xVal;
}
double y() const {
return yVal;
}
private:
double xVal;
double yVal;
};
#endif
上例子说明了C++的几个特性:
- 类定义可以划分为 public、protected、private 三段,且以一个分号结束。如果没有定义段,默认是private段。
- 类的构造函数方便同Java语言(如果没有定义,则C++会自动提供一个无参构造函数)。
- 用来获取值的函数 x() 和 y() 声明为const。这就意味着它们不会(而且也不能)修改成员变量或者调用非const成员函数。
上述的这些函数都是实现为了内联函数(inline),也即是类定义的一部分。另一种方式是只把函数原型放在头文件中,而把函数的代码放在 .cpp中。使用这种方式时,头文件看起来应该是这样的:
#ifndef POINT2D_H
#define POINT2D_H
class Point2D {
public:
Point2D();
Point2D(double x, double y);
void setX(double x);
void setY(double y);
double x() const;
double y() const;
private:
double xVal;
double yVal;
};
#endif
于是就可以在point2d.cpp 文件中实现这些函数:
#include "point2d.h"
Point2D::Point2D() {
xVal = 0.0;
yVal = 0.0;
}
Point2D::Point2D(double x, double y) {
xVal = x;
yVal = y;
}
void Point2D::setX(double x) {
xVal = x;
}
void Point2D::setY(double y) {
yVal = y;
}
double Point2D::x() const {
return xVal;
}
double Point2D::y() const {
return yVal;
}
文件是从包含 point2d.h 开始的,因为编译器需要在它分析类的成员函数的实现之前要先知道类的定义。然后,再实现这些函数,并且在函数的名字前加上 :: 操作符和类名一起构成的前缀。
前面看到了如何把一个函数实现为内联函数的方法,现在又看到了如何在.cpp文件中实现它。从语法上讲,这两种方法是等效的,但是当我们调用一个声明为内联函数的函数时,绝大多数的编译器都只是简单的扩展其函数体,而不会生成实际的函数调用。这种通常可以产生更为快速的代码,但是可能会增加应用程序的大小。基于这样的原因,只有非常简短的函数才应该实现为内联函数,比较长的函数总是应该在.cpp文件中加以实现。
尝试使用类:
#include <iostream>
#include "point2d.h"
using namespace std;
void main() {
Point2D a;
Point2D b(2, 3);
a.setX(b.y());
b.setY(a.x());
}
在C++中,任意类型的变量都可以直接声明而不必一定要使用new。第一个变量会使用默认的构造函数(无参)进行初始化。第二个变量则使用第二个构造函数进行初始化。对一个对象的成员进行访问需要使用 . 操作符。
以这种方式声明变量的行为就像java中声明一些基本类型一样,如int 、double等。例如,当使用赋值操作符时,会复制变量的内容,而不是复制对象的引用(reference)。如果要在以后修改一个变量值,那么从他那里赋值而来的其他变量都仍旧保持不变。
2. 继承和多态
作为一种面向对象语言,C++支持继承和多态。举例说明它是如何工作的:
以Shape 为抽象基类,以Circle为子类。先从基类开始,
#ifndef SHAPE_H
#define SHAPE_H
#include "point2d.h"
class Shape {
public:
Shape(Point2D center) {
myCenter = center;
}
virtual void draw() = 0;
protected:
Point2D myCenter;
};
#endif
这个定义放在头文件shape.h 中。由于在这个类的定义中引用了类Point2D,所以需要包含头文件point2d.h。
类Shape没有基类。这点和Java不同,C++没有为所有类提供一个可以从中继承出来的一般类Object。Qt则为所有类型的对象提供了一个简单的基类QObject。
draw() 函数的声明有两个特点:它含有 virtual关键字,并且以 =0 结束。关键字virtual 表明这个函数可能会在子类中重新得到实现。就像C#中一样,C++成员函数在默认情况下也是不能重新实现的。=0 的语句表明这个函数时一个纯虚函数(pure virtual function)–一个没有默认实现代码并且必须在子类中实现的函数。
子类Circle的定义:
#ifndef CIRCLE_H
#define CIRCLE_H
#include "shape.h"
class Circle : public Shape {
public:
Circle(Point2D center, double radius = 0.5)
: Shape(center) {
myRadius = radius;
}
void draw() {
//...
}
private:
double myRadius;
};
#endif
类 Circle 通过公有(public)方式继承了 Shape,也就是说,Shape中的所有公有成员在 Circle 中仍旧是公有的。C++也支持保护(protected)继承和私有(private)继承,利用它们可以限制对于基类public成员和protected成员的访问(参见:[http://blog.csdn.net/complety/article/details/7493194])。
该类的构造函数有两个参数。第二个参数是可选的,并且如果没有给定参数值就会取0.5.构造函数在函数名和函数体之间使用了一种特殊的语法把center参数传递给基类的构造函数。在函数体中,对成员变量myRadius进行了初始化。
注意,C++不允许在类定义中初始化成员变量(只有静态常量整型数据成员才可以在类中初始化)。因此,下面的代码是错误的:
//编译错误
private:
double myRadius = 0.5;
};
该类的draw()函数与Shape中声明的虚函数draw()具有相同的名字。它是该函数的一个重新实现,并且在Circle实例上通过Shape引用或者指针调用draw()时,就会以多态的形式调用该函数。C++不像java那样有override关键字。而且C++也没有能够指向基类的super或base关键字。如果需要调用一个函数的基本实现,则可以在这个函数的名字前加上一个由基类的名字和 :: 操作符构成的前缀。例如:
#include "Circle.h"
class LabeledCircle : public Circle {
public:
void draw() {
Circle::draw();
drawLabel();
}
private:
void drawLabel();
};
C++支持多继承,也就是,一个类可以同时从多个类中派生出来。语法形式如下所示:
class DerivedClass : public BaseClass1, public BaseClass2,..., public BaseClassN
{
//...
}
默认情况下,类中声明的函数和变量都与这个类的实例相关。我们也可以声明静态(static)成员函数和静态成员变量,可以在没有实例的情况下使用它们。如下:
#ifndef TRUCK_H
#define TRUCK_H
class Truck {
public:
Truck() {
++counter;
}
~Truck() {
--counter;
}
static int instanceCount() {
return counter;
}
private:
static int counter;
};
#endif
通过这里的静态成员变量counter,我们可以在任何时候知道还有多少个Truck实例。Truck的构造函数会增加它的值。通过前缀 ~ 识别的析构函数(destructor)可以减少它的值。在C++中,在静态分配的变量超出作用域或者是在删除一个使用new 分配的变量时会自动调用这个析构函数。除了我们可以在某个时刻调用析构函数这一点外,其他与Java中的finalize() 方法相似。
一个静态成员变量在一个类中只有单一的存在实体:这样的变量就是“类变量”(class variable)而不是“实例变量”(instance variable)。
每一个静态成员变量都必须定义在.cpp 文件中(但不能再次重复static关键字)。例如:
#include <iostream>
#include "truck.h"
using namespace std;
int Truck::counter = 0;
int main() {
Truck truck1;
Truck truck2;
cout<< Truck::instanceCount() << endl;
return 0;
}
不在.cpp 文件中定义,会在连接时产生一个“unresolved symbol”(不可解析的符号)的错误信息。只要把类名作为前缀,就可以在该类外面访问这个instanceCount() 静态函数。
3. 指针
C++中,指针就是一个可以存储对象的内存地址的变量(而不是直接存储这个对象)。Java和C#都有类似的概念—“引用”(reference),但是在语法上并不相同。如下例子来说明指针的用法:
#include <iostream>
#include "point2d.h"
using namespace std;
int main() {
Point2D a;
Point2D b;
Point2D *ptr;
ptr = &a;
ptr->setX(1);
ptr->setY(2);
ptr = &b;
ptr->setX(4);
ptr->setY(5);
ptr = 0;
cout<< a.x();
return 0;
}
第7,8行定义了两个Point2D对象。根据Point2D 的默认构造函数,这两个对象将被初始化为(0, 0)。
第9行定义了一个指向Point2D类型变量的指针。指针的语法是在变量名前面再加上一个 *。由于没有初始化这个指针,所以它包含的是一个随机的内存地址值。通过第11行给这个指针分配了a对象的地址就完成了对这个指针的初始化。这里的一元运算符 & 可以返回一个对象的内存地址值。地址值通常是一个32位或64位的整型值,可以用来确定一个对象在内存中的偏移量。
在第12,13行,我们通过 ptr 指针来访问 a 对象。因为 ptr 是指针而不是对象,所以必须使用 -> 操作符代替 . 操作符。
在第15行,我们把 b 的地址赋值给了这个指针。于是从此时开始,通过这个指针执行的任何操作都将会影响 b 对象。
第19行,把这个指针设置为空(null)指针。C++没有一个可以用于表示不指向任何对象指针的关键字。所以,我们换用值0(或者是符号常量NULL,他可以扩展为0)来代替。试图使用一个空指针会造成系统的崩溃,其提示的错误信息有“段错误(Segmentation fault)”、“常规保护错误”(general protection fault)或者是“总线错误”(Bus error)等。
指针通常用于存储使用 new 动态分配的对象(与普通对象的区别?)。在C++术语中,把这样的对象成为是 分配在“堆”(heap)上,而局部变量(在一个函数中定义的变量)则存储在“栈”(stack)里。
这里给出了一段用来说明使用new进行动态内存分配的代码片段:
#include "point2d.h"
void main() {
Point2D *point = new Point2D;
point->setX(1);
point->setY(2);
delete point;
}
new 操作符返回一个新近分配对象的内存地址。我们把这个地址存储在一个指针变量中,并且通过这个指针访问该对象。当处理完这个对象后,就可以使用delete 关键字释放它的内存。不像Java和C#,C++没有垃圾收集器,当不再需要那些动态分配的对象时,就必须明确使用delete来释放它们。
如果忘记调用 delete,则内存就会一直保留到该程序结束时为止。这在上面的例子中国不是什么大问题,因为我们只是分配了一个对象,但是如果在一个总是需要不断分配新对象的程序中,就可能造成程序总是分配内存,就可能将机器的内存耗尽。对象一旦删除,则指向该对象的指针变量仍旧会保存这个对象的地址值。这样的指针称为“悬摆指针”(dangling pointer),最好不要再使用这样的指针访问该对象。
上面例子中,调用了默认的构造函数并且调用setX()和setY()来初始化该对象。我们本应当使用带有两个参数的构造函数来代替默认的构造函数:
Point2D *point = new Point2D(1, 2);
这个例子并不需要使用 new 和 delete。(??)我们最好也像下面那样在栈上分配该对象:
Point2D *point;
point->setX(1);
point->setY(2);
向这样分配的对象会在出现它们的程序块的末尾自动得到释放。
如果不打算通过该指针来修改这个对象,则可以把指针声明为const型指针。如下:
const Point2D *point = new Point2D(1, 2);
int x = point->x();
//error
point->setX(1);
point->setY(2);
ptr = Point2D(2, 4);
这个常量指针point 只能用于调用常量成员函数,比如x()和y()。当不打算使用指针修改它们时,把指针生命成const是一种不错的习惯。而且,如果该对象自身就是常量,那么我们就没有什么选择了,只能使用常量指针来存储它的地址值。const 的用法可以为编译器提供一定的信息,这可以提早发现一些bug,并获得良好的性能。
指针既可以用在内置类型上,也可以用在类上。需要说明的是,一元运算符 * 可以返回与这个指针相关的对象的值。例如:
void main() {
int i = 10;
int j = 20;
int *p = &i;
int *q = &j;
cout<< *p << " equals 10" <<endl;
cout<< *q << " equals 20" <<endl;
*p = 40; //取得指针类型的变量p所指向的值
cout << i << " equals 40" << endl;
}
箭头运算符 -> 可以用于通过指针来访问对象的成员,这纯粹是一种语法甜头(syntactic sugar)。除了 ptr->member 的形式之外,还可以使用 (*ptr).member 的形式。这里的圆括号是必须的,因为 . 运算符比 运算符具有更高的运算优先级。
4.引用
除了指针,C++也支持“引用”的概念。像指针一样,一个C++的引用存储的也是一个对象的地址值。两者的主要不同点在于:
- 声明引用时使用的是 & 而不是 *;
- 引用必须是初始化过的,并且不能在后面再次重新赋值;
- 可以直接访问与引用相关联的对象, 且没有像 * 或者 -> 这样的特殊语法;
- 引用不能为空(null)。
在声明参数时,会经常使用到引用。对于大多数类型来说,C++会使用按值调用(call by value)的方式来作为它的默认参数传递机制。也就是说,当给一个函数传递参数的时候,该函数会接收到这个对象的一个新的副本。这里给出一个函数的定义,它就是通过按值调用的方式来接收它的参数值的:
#include <iostream>
#include "point2d.h"
#include <cstdlib>
using namespace std;
double manhattanDistance(Point2D a, Point2D b) {
return abs(b.x() - a.x()) + abs(b.y() - a.y());
}
于是就可以按照如下方式来调用该函数:
void main() {
Point2D a(12, 40);
Point2D b(30, 4);
double distance = manhattanDistance(a, b);
}
C程序员通过把参数声明为指针而不是值的方式,能够避免不必要的复制操作:
double manhattanDistance(const Point2D *ap, const Point2D *bp) {
return abs(bp->x() - ap->x()) + abs(bp->y() - ap->y());
}
于是,在调用该函数时传递的必须是地址而不是值:
double distance = manhattanDistance(&a, &b);
C++引入引用的概念而使得语法变得更为简单,并且还可以使调用者避免出现传递空指针的现象。如果使用的是引用而不是指针,那么该函数会是下面这样:
double manhattanDistance(const Point2D &a, const Point2D &b) {
return abs(b.x() - a.x()) + abs(b.y() - a.y());
}
引用的声明与指针的声明有点相似,只是用的是 & 而不是 *。但是当我们实际使用引用的时候,无需记住它是一个内存地址,而只需把它看作一个普通的变量就可以了。另外,调用一个带有引用作为参数的函数时,并不需要给予太多的考虑(不带 & 运算符)。调用方式如下:
double distance = manhattanDistance(a, b);
总而言之,通过在参数列表中吧Point2D 替换成 const Point2D &,就可以降低函数调用的开销:不用再复制256位(4个double值的大小),只需要复制64位或128位–这取决于目标平台指针的大小。
在前一个例子中使用const引用,就可以避免函数修改与这些引用相关联的对象。但是当我们需要这种特殊效果时,则可以传递一个非常量引用或者一个指针。例如:
void tanspose(Point2D &point) {
double oldX = point.x();
point.setX(point.y());
point.setY(oldX);
}
在某些情况下,我们有一个引用并且需要调用一个带指针的函数,或者相反的情形。要把引用转换成指针,可以使用一元操作符 &:
Point2D point;
Point2D &ref = point; //引用是变量的别名
Point2D *ptr = &ref; //等价于 Point2D *ptr = &point;
要把指针转换成引用,可以使用一元操作符 *:
Point2D point;
Point2D *ptr = &point;
Point2D &ref = *ptr;
引用和指针在内存中的表达方式一样,并且在使用时会经常互换它们,这需要根据具体问题来具体确定到底应该使用哪一种形式。
注:引用的详细解释,参见博客 《C++中引用(&)的用法和应用实例》。
5. 数组
在C++中,数组的声明的语法和java类似。但是在声明一个数组时,需要指明数组所包含元素的个数。
静态初始化也可以用于复杂数据类型,如下:
Point2D trangle[] = {Point2D(0, 0), Point2D(0, 1), Point2D(1, 1)};
如果后续程序中无需修改数组,可以让它变成常量型数组:
const Point2D trangle[] = {Point2D(0, 0), Point2D(0, 1), Point2D(1, 1)};
计算数组长度,可以使用sizeof()运算符:
int n = sizeof(trangle) / sizeof(trangle[0]);
数组的遍历通常是使用整数来完成的。例如:
for (int i = 0; i < n; ++i)
{
cout<< fibonacci[i];
}
也可以使用指针来遍历数组:
const int *ptr = &fibonacci[0];
while(ptr != &fibonacci[10]) {
cout<< *ptr;
++ptr;
}
不使用&fibonacci[0],应该也可以完成对fibonacci的写操作。这是因为单独使用一个数组的名字会自动转换为指向该数组中第一个元素的指针。与此类似,可以使用fibonacci + 10 来代替&fibonacci[10]。这种方式也能够很好的工作:我们可以使用ptr或者ptr[0]获得当前元素的内容,并且可以通过(ptr + 1)或者ptr[1]访问下一个元素。这一原理有时称为“指针和数组的等价性”。
const int *ptrr = fibonacci;
while(ptrr != (fibonacci + 10)) {
cout<< *ptrr;
ptrr++;
}
为了避免出现无故的低效率,C++不允许我们给函数传递数组的值。相反,它们必须以地址的形式传递。例如:
void printIntegerTable(const int *table, int size) {
for (int i = 0; i < size; i++)
{
cout<< table[i];
}
}
void main() {
int fibonacci[] = {0, 1, 1, 2, 3, 5, 8, 13, 21, 34};
printIntegerTable(fibonacci, 10);
}
当声明一个C++数组的时候,数组的大小必须是一个常数值。如果希望创建一个可变大小的数组,可以有多种方法。
1. 动态分配该数组
int *fibonacci = new int[n];
这个new[]运算符会在内存的连续位置分配一定数量的元素,并且可以返回一个指向第一个元素的指针。由“指针和数组的等价性”原理可知,可以通过该指针访问元素fibonacci[0],..fiboncacci[n-1]。当时用完数组后,应当使用运算符delete[]释放它所占用的内存空间。
delete[] fibonacci;
2.使用标准的 std::vector类
#include<vector>
std::vector<int> fibonacci(n);
使用[]运算符可以访问各个元素,就像访问普通C++数组一样。利用std::vector(这里的T是存储在向量中的元素类型值),可以在任何时候使用resize()改变这个数组的大小,并且可以使用赋值运算符复制它。在类的名字中包含尖括号的类称为模板类。
6.字符串
C++中,字符串最为基本的表达方式就是使用一个以空字节(’\0’)为结束符的字符数组。下面的4个函数给出了字符串的这些工作方式:
void hello1() {
const char str[] = {'H', 'O', '\0'};
cout<< str;
}
void hello2() {
const char str[] = "Hello world!";
cout << str;
}
void hello3() {
cout<< "Hello world!";
}
void hello4() {
const char *str = "Hello world!";
cout << str;
}
第一个函数中,把字符串声明为一个数组,并且艰难的对其进行了初始化。需要注意结尾处的“\0”结束符,它表明这里是字符串的结尾。第二个函数具有相似的数组定义形式,但是这一次使用一串字符文字来初始化数组。在C++中,字符串中的文字是以隐式“\0”结束符结尾的简单常量字符数组。第三个函数直接使用一串字符文字,而没有给它指定名字。但一旦转化成机器语言指令,它就与前面的两个函数一样。
第四个函数则有一点不同,在于它创建的不仅是一个(匿名)数组而且是一个称为str的指针变量,并且用这个指针来存储该数组第一个元素的地址。
以C++字符串作为参数的函数通常都带有char*,或者const char*。这里给出的一小段程序显式了这两种方法的用法:
void makeUppercase(char *str) {
for (int i = 0; str[i] != '\0'; ++i)
{
str[i] = toupper(str[i]);
}
}
void writeLine(const char *str) {
cout << str;
}
在C++中,char 类型通常保存为8位的值。这就是说,我们可以在一个char数组中存储ASCII以及其他采用8位编码的字符串,但是在没有使用多字节序列的情况下,就不能存储任意的Unicode字符。
7.枚举
略。
8.类型别名
C++允许我们使用关键字 typedef 把一个数据类型设定成其他名字(别名)。例如,如果需要多次使用 QVector,就可以通过把这个typedef声明放在某个头文件中:
typedef QVector<Point2D> PointVector;
从此以后,就可以把PointVector 当做QVector的缩略形式。主义,类型的新名字需要放在旧名字后面。typedef 的语法有些故意的模仿了变量声明的形式。
9.类型转换
待补充。
10.运算符重载
C++允许我们重载函数。另外,C++也支持运算符重载(operator override)。也就是说,当需要在自定义的类型内使用内置运算符(比如+、<<和[])时,就可以给它们分配特殊的语义。
我们已经看到了许多运算符重载的例子。当使用<<把文字输出到cout或者cerr时,我们并没有触发C++的左移运算,而是将其作为运算符的一种特殊使用形式:左侧带一个ostream对象(比如cout和cerr),右侧带有一个字符串(或者,也可以是一个数字或者是一个流控制器,比如endl),返回的是该ostream对象,而且也允许在一行中多次调用。
运算符重载的巧妙之处在于我们可以让自定义类型的行为表现的像使用内置类型的行为一样。为了说明运算符重载是如何工作的,我们将会在Point2D对象的基础上重载 +=,-=, +和-运算符:
#ifndef POINT2D_H
#define POINT2D_H
class Point2D {
public:
Point2D();
Point2D(double x, double y);
void setX(double x);
void setY(double y);
double x() const;
double y() const;
//operator override
Point2D &operator+=(const Point2D &other) {
xVal += other.xVal;
yVal += other.yVal;
return *this;
}
Point2D &operator-=(const Point2D &other) {
xVal -= other.xVal;
yVal -= other.yVal;
return *this;
}
private:
double xVal;
double yVal;
};
inline Point2D operator+(const Point2D &a, const Point2D &b) {
return Point2D(a.x() + b.x(), a.y() + b.y());
}
inline Point2D operator-(const Point2D &a, const Point2D &b) {
return Point2D(a.x() - b.x(), a.y() - b.y());
}
#endif
运算符可以被初始化为成员函数或者全局函数。在例子中,我们把+=和-=实现为成员函数,把+和-实现为全局函数。
+=
和 -=
运算符带有一个指向另一个Point2D对象的引用,并在其他对象的x坐标和y坐标基础上,对当前对象的x坐标和y坐标进行增或减运算。它们返回的this,该值表示一个指向当前对象(它的类型是Point2D)的引用。利用返回的这个引用,就可以写出特殊形式的代码,比如:
a += b += c;
+
和-
运算符带两个参数,并且通过变量返回一个Point2D对象。inline 关键字允许我们把这些函数的定义放在头文件中。如果某个函数的函数体比较长,那么将会把该函数的函数原型放在头文件中,然后把该函数的定义(不带inline 关键字)放在一个cpp文件中。
下列代码片段给出了编程应用中4中运算符的重载方法:
Point2D a(1, 2);
Point2D b(3, 4);
a += b;
b -= a;
Point2D c = a + b;
Point2D d = b - a;
也可以像调用其它函数一样来调用运算符函数:
Point2D a(1, 2);
Point2D b(3, 4);
a.operator+=(b);
b.operator-=(a);
Point2D c = operator+(a, b);
Point2D d = operator-(b, a);
在C++中,运算符重载时一个复杂的话题,但是在不必详知所有细节的情况下我们仍旧可以使用C++。但是了解运算符重载的基本原理还是非常重要的,因为有多个Qt类就是利用这一特性来提供一种更为简单和自然的语法的,比如字符串的连接和追加等操作。
11.值类型
值类型和引用类型:
值类型适用于基本类型,如char、int和float。区分它们的主要特征在于创建它们时并不需要使用new。还有,在执行赋值运算时会对变量持有者进行复制。例如:
int i = 5;
int j = 10;
i = j;
引用类型适用于一些类,比如Integer(在Java中)、String。实例是通过new创建的。执行赋值运算时,只是对指向这个对象的引用的复制。要想获得深度复制效果,必须调用函数clone()(在Java中)或者Clone(在C#中)。例如:
Integer i = new Integer(5);
Integer j = new Integer(10);
i = j.clone();
在C++中,所有类型都可以用作“引用类型”,并且那些具有可复制性的类型也可以用作“值类型”。例如,C++不需要任何Integer类,因为我们可以像下面这样使用指针和new:
int *i = new int(5);
int *j = new int(10);
*i = *j;
不像Java和C#,C++会像对待内置类型一样对待用户自定义的类:
Point2D *i = new Point2D(5, 5);
Point2D *j = new Point2D(10, 10);
*i = *j;
如果想让某个C++类具备可复制性,则必须确保类有一个复制构造函数(copy constructor)和一个赋值运算符。当用同一种类型的对象初始化另一个对象时,就会调用复制构造函数。对于这一操作,C++提供了两种等价的语法:
Point2D a(1, 2);
Point2D c (a); //first syntax
Point2D d = a; //second syntax
当在一个已存在的变量上调用赋值运算符的时候,就会调用该赋值运算符:
Point2D a(1, 2);
Point2D b(3, 4);
a = b;
在定义一个类时,C++编译器会自动提供一个复制构造函数和一个赋值运算符,以用于执行成员到成员的复制。对于这个Point2D类,这样做就相当于在这个类的定义中写下了下列代码:
class Point2D {
public:
...
Point2D(const Point2D &other): xVal(other.xVal), yVal(other.yVal){};
//operator override
Point2D &operator=(const Point2D &other) {
xVal = other.xVal;
yVal = other.yVal;
return *this;
}
...
private:
double xVal;
double yVal;
};
对于某些类,默认的复制构造函数和赋值运算符可能都不够用。比如当这些类使用的是动态内存时,通常都会出现这种情况(??)。要让该类具有可复制性,就必须自己实现它的复制构造函数和赋值运算符。
对于一些不必具有可复制性的类,可以通过让复制构造函数和赋值运算符成为私有类型而禁用它们。
12.全局变量和全局函数
C++允许声明一些不属于任何类的函数和变量,并且这些函数和变量可以被其他的任意函数访问。我们已经看到了多个全局函数的例子,包括作为程序入口的main()函数。全局变量还没有看到,因为它们需要在程序的模块和线程之间来回重复以求取折衷(??)。但理解它们还是很重要的,因为我们可能在在C和C++中碰到它们。
为了能够举例说明全局函数和全局变量是如何工作的,下面给出一段程序作为例子:使用quick-and-dirty算法打印一个由128个伪随机数构成的列表。
第一个.cpp文件的源文件是random.cpp:
int randomNumbers[128];
static int seed = 42;
static int nextRandomNumber() {
seed = 1009 + (seed * 2011);
return seed;
}
void populateRandomArray() {
for (int i = 0; i < 128; ++i)
{
randomNumbers[i] = nextRandomNumber();
}
}
这个文件声明了两个全局变量(randomNumbers 和 seed )和两个全局函数(nextRandomNumber()和populateRandomArray())。其中的两个声明包含关键字static,这样的声明只有在当前编译单元(random.cpp)中才是可见的,成为 静态连接(satic linkage)。其他两个则可以从程序的任意编译单元中访问,称为 外部连接(external linkage)。
静态连接适用于那些不需要再其他编译单元中使用的帮助函数和内部变量。他可以降低标识符(具有同样名字的全局变量或者是在不同编译单元中具有同样署名的全局函数)冲突的风险,并且可以防止误访问一个编译单元的内部。
第二个文件,main.cpp,它使用了在random.cpp文件中用外部连接声明的两个全局变量:
#include<iostream>
using namespace std;
extern int randomNumbers[128];
void populateRandomArray();
void main() {
populateRandomArray();
for (int i = 0; i < 128; i++)
{
cout<< randomNumbers[i]<< endl;
}
}
在使用外部变量和调用外部函数之前,需要先声明它们。对于 randomNumbers 的外部变量声明(可以让一个外部变量在当前编译单元中可见)以extern关键字开始。没有extern,编译器就会认为它需要处理的是一个不确定的定义,这样会导致连接器报错,因为同一变量同时在两个编译单元(random.cpp和 main.cpp)中都被定义了。只要需要,可以任意多次的声明变量,但是只能定义它们一次。定义(definition)就是让编译器为该变量保留内存空间。
populateRandomArray() 函数时通过原型声明的。对于函数,extern可有可无。
通常,会把external 变量和函数声明放在头文件中,并且把该文件在所有需要它们的文件中包含一次:
#ifndef RANDOM_H
#define RANDOM_H
int randomNumbers[128];
void populateRandomArray();
#endif
我们已经看到了如何使用static 来声明一些不属于任何一个类实例的成员变量和成员函数,并且现在也看到了如何使用它来声明静态连接的函数和变量。static 关键字还有另一种用法,在C++中,可以声明一个局部静态变量(local static variable)。这样的变量会在第一次调用函数时得到初始化。
13.命名空间
命名空间是一种用于减少C++程序中名字冲突的机制。在使用了第三方软件库的大程序中,名字冲突是一个常常遇到的问题。在自己的程序中,可以选择是否使用命名空间。
通常,我们把命名空间放在头文件中所有声明的周围,以确保在该头文件中声明的所有标识符不会与全局命名空间中的标识符相冲突。例如:
#ifndef RANDOM_H
#define RANDOM_H
namespace SoftwareInc {
int randomNumbers[128];
void populateRandomArray();
}
#endif
命名空间的语法与类的语法相仿,但是它不以分号结束。这里是random.cpp文件的新形式:
#include "random.h"
int SoftwareInc::randomNumbers[128];
static int seed = 42;
static int nextRandomNumber() {
seed = 1009 + (seed * 2011);
return seed;
}
void SoftwareInc::populateRandomArray() {
for (int i = 0; i < 128; ++i)
{
randomNumbers[i] = nextRandomNumber();
}
}
与类不同,可以在任何时候“重新打开”命名空间。例如:
namespace Alpha {
void alpha1();
void alpha2();
}
namespace Beta {
void beta1();
}
namespace Alpha {
void alpha3();
}
这就使得定义许多类成为可能,可以把这些类放在多个头文件中,然后再由这些文件构成一个命名空间。利用这一技巧,C++标准库就把它的所有标识符都放在std命名空间中。
要在命名空间外部引用该命名空间中的一个标识符,可以把命名空间(和一个::)作为该表示的前缀。或者,也可以使用以下三种机制之一来做到这一点,这三种方法的目的都是尽量减少敲击键盘的次数。
1.定义命名空间的别名
namespace ElPulbloDelDeLosAngles {
void malibu();
}
namespace LA = ElPulbloDelDeLosAngles;
2.从命名空间中导入一个简单的标识符
int main() {
using ElPulbloDelDeLosAngles::malibu;
malibu();
}
3.只用一条指令导入整个命名空间
int main() {
using namespace ElPulbloDelDeLosAngles;
santaMonica();
malibu();
}
使用了这种方法,似乎更容易产生名字冲突了。如果编译器抱怨有二义性的名字(在两个不同命名空间中定义了两个具有相同名字的类),那么在需要引用这个名字的时候,通常就要使用命名空间来限定这个标识符。
转载请注明出处:http://blog.csdn.net/u014656992/article/details/53289997