C++程式设计允许程序员使用(class)定义特定程序中的数据类型。这些数据类型的实例被称为对象 ,这些实例可以包含程序员定义的成员变量常量成员函数,以及重载的运算符。语法上,类似C中结构体(struct)的扩展,C中结构体不能包含函数以及重载的运算符。

C 结构体与C++ 类的对比

编辑

在 C++ 中,结构体 是由关键词 struct 定义的一种数据类型[1]。他的成员和基类默认为公有的(public)。由关键词 class 定义的成员和基类默认为私有的(private)。这是C++中结构体和类仅有的区别。

聚合类

编辑

聚合类是一种没有用户定义的构造函数,没有私有(private)和保护(protected)非静态数据成员,没有基类,没有虚函数[2]。这样的类可以由封闭的大括号用逗号分隔开初始化列表[3]。下列的代码在 C 和 C++ 具有相同的语法:

struct C
{
  int a;
  double b;
};

struct D
{
  int a; 
  double b;
  C c;
};

// initialize an object of type C with an initializer-list
C c = { 1, 2 };

// D has a sub-aggregate of type C. In such cases initializer-clauses can be nested
D d = { 10, 20, { 1, 2 } };

POD 结构

编辑

一个POD结构(普通旧式数据结构)是一个不包含非POD结构、非POD联合(或者这些类型的数组)或引用的非静态成员变量(静态成员没有限制),并且没有用户定义的赋值运算符析构器的聚合类。[1] 一个POD结构可以说是C struct在C++中的等价物。在大多数情况下,一个POD结构拥有和一个在C中声明的对应的结构相同的内存布局。[4]因此,POD结构有时不正式地被称为“C风格结构”(C-style struct)。 [5]

C结构与C++ POD结构共有的属性

编辑
  • 数据成员被分配使得一个对象中之后的成员有着更高的地址,除非跨越了一个访问描述符[6]
  • 两个POD结构类型是布局兼容的如果它们有相同数量的非静态数据成员,而且对应的非静态数据成员(按照顺序)是布局兼容的[7]
  • 一个POD结构可以包含未命名的填充[8]
  • 一个指向POD结构对象的指针适合使用reinterpret_cast,指向其初始成员而且反之亦然,说明在POD结构的头部不存在填充[8]
  • 一个POD结构可以被offsetof宏使用[9]

声明和使用

编辑

C++ 的结构体和类具有他们自己的成员。这些成员包括变量(包括其他结构体和类),被看做方法的函数(特定的标示符或重载的运算符),构造函数以及析构函数。成员被声明成为公共或私有使用说明符public:private:来区分。说明符后出现的任何成员会获得相应的访问权限直到下一个说明符的出现。对于继承的类能够使用protected:说明符。

基本声明和成员变量

编辑

类和结构体的声明使用关键词classstruct。成员在类和结构体的内部声明。

下面的代码段实例了结构体和类的声明:


struct person
{
  string name;
  int age;

};
class person
{
public:
  string name;
  int age;
};

以上两个声明在功能上是等价的。每一段代码都定义了一个类型person,其含有两个成员变量:nameage。注意,大括号后面的分号是必需的。

在其中一个声明之后(不能同时使用两个),person可以被用来定义新的person类型的变量:

#include <iostream>
#include <string>
using namespace std;

class person
{
public:
  string name;
  int age;
};

int main ()
{
  person a, b;
  a.name = "Calvin";
  b.name = "Hobbes";
  a.age = 30;
  b.age = 20;
  cout << a.name << ": " << a.age << endl;
  cout << b.name << ": " << b.age << endl;
  return 0;
}

执行以上代码将会输出

Calvin: 30
Hobbes: 20

成员函数

编辑

成员函数是C++ 的类和结构体的一个重要特性。这些数据类型可以包含作为其成员的函数。成员函数分为静态成员函数与非静态成员函数。静态成员函数只能访问该数据类型的对象的静态成员。而非静态成员函数能够访问对象的所有成员。在非静态成员函数的函数体内,关键词this指向了调用该函数的对象。这通常是通过thiscall调用协议,将对象的地址作为隐含的第一个参数传递给成员函数。[10]再次以之前的person类型作为例子:

class person
{
  std::string name;
  int age;
public:
  person() : age(5) { }
  void print() const;
};

void person::print() const
{
  cout << name << ";" << this->age << endl;
  /* "name"和"age"是成员变量。
     "this"关键字的值是被调用对象的地址。其类型为
     const person*,原因是该函数被声明为const。
  */
}

在上面的例子中print()函数在类中声明,并在类的名称后加上::来限定它后加以定义。nameage是私有的(类的默认修饰符),print()被声明为公有,由于一个被用于类外的成员需要被申明为公有的。

通过使用成员函数print(),输出可以被简化为:

a.print();
b.print();

上述的ab被称为调用者(sender),当print()函数被执行时每一个都引用自己的成员变量。 将类或结构的申明(称做接口)和定义(称作实现)放入分开的单元是常见的做法。用户需要的接口被放入一个头文件中而实现则独立地放入原始码或者编译后的形式。

非静态成员函数,可以用const或volatile关键词限定。const限定的成员函数不能修改其他数据成员(除了具有mutable的例外),也不能调用非const限定的其他成员函数。编译实现时,通常是在const限定的成员函数体内,this所指向的数据成员自动具有const限定,因此是只读的。const对象只能调用const成员函数;volatile对象只能调用volatile限定的成员函数。反之,没有受到限定的普通对象可以调用所有的成员函数,不论它是否为cv限定。构造函数、析构函数不能cv限定。

继承

编辑

非POD类的内存布局没有被C++标准规定。例如,许多流行的C++编译器通过将父类的字段和子类的字段并置来实现单继承,但是这并不被标准所需求。这种布局的选择使得将父类的指针指向子类的操作是平凡的(trivial)。

例如,考虑:

class P 
{
    int x;
};
class C : public P 
{
    int y;
};

一个P的实例和P* p指向它,在内存中可能看起来像这样:

+----+
|P::x|
+----+
↑
p

一个C的实例和P* p指向它,在内存中可能看起来像这样:

+----+----+
|P::x|C::y|
+----+----+
↑
p

因此,任何操纵P对象的字段的代码都可以操纵在C对象中的P字段而不需要考虑任何关于C字段的定义。一个正确书写的C++程序在任何情况下都不应该对被继承字段的布局有任何假定。使用static_cast或者dynamic_cast类型转换运算符会确保指针正确的从一个类型转换为另一个。

多重继承并不那么简单。如果一个类D继承了P1P2,那么两个父类的字段需要被按照某种顺序存储,但是(在大多数情况下)只有一个父类可以被放在子类的头部。每当编译器需要将一个指向D的指针转换为P1P2中的任一个,编译器需要提供一个自动转换从子类的地址转换为父类字段的地址(典型地,这是一个简单的偏移量计算)。

关于多重继承的更多资讯,参看虚继承

重载运算符

编辑

C++容许程序员重载某些运算符,目的是补充库中未能提供的针对特定类的运算符。同理,很多时自定类也因为内建库不能提供指定运算符而需要重载。

另外,当程序员没有重载或定义某些运算符时,编译器会自动地建立它们,例如三法则中的复制指定运算符(=)。

依照惯例,重载运算符时应模拟运算符本身意义的功能,例如重载运算符“*”时,程序员义务重载为两数之乘法(或其他,视数学或程序上的意义)。另外,宣告一结构如integer类,当重载运算符如“*”就要回传integer类:

struct integer 
{
    int i;
    integer(int j = 0) : i(j) {}
    integer operator*(const integer &k) const 
    {
        return integer (i * k.i);
    }
};

struct integer 
{
    int i;
   
    integer(int j = 0) : i(j) {}
 
    integer operator*(const integer &k) const;
};
 
integer integer::operator*(const integer &k) const 
{
    return integer(i * k.i);
}

在这里,const关键字出现两次。表达式const integer &k中的const关键字代表函数不能修改此常量值,而第二个const关键字代表此函数不会修改类对象本身(*this)。

integer &k之中,符号(&)表示以引用形式调用。当调用函数时会直接传递变量地址,并以变量本身取代这里的变量k[11]

二元可重载运算符

编辑

二元运算符会用函数方式并以“operator 運算符”识别来进行重载,这里的参数会是单一参数。实际使用时,二元运算符左方的变量会成为类对象本身(*this),而右方变量则成为传入参数。

integer a = 1; 
/* 這裡的等號是其中一種二元運算符,
   我們可利用重載運算符(=)的方式
   來提供初始化功能,而左方的變數i
   就是類物件本身,右方的數字1則是
   傳入參數。 */
integer b = 3;
/* 變數名字跟類物件內的變數無關 */
integer k = a * b;
cout << k.i << endl;  //輸出3

以下是二元可重载运算符列表:

算术运算符

运算符名称 语法
加法(总和) a + b
以加法赋值 a += b
减法(差) a - b
以减法赋值 a -= b
乘法(乘积) a * b
以乘法赋值 a *= b
除法(分之) a / b
以除法赋值 a /= b
模数(余数) a % b
以模数赋值 a %= b

比较运算符

运算符名称 语法
小于 a < b
小于或等于 a <= b
大于 a > b
大于或等于 a >= b
不等于 a != b
等于 a == b
逻辑 AND a && b
逻辑 OR a || b

位操作子

运算符名称 语法
位元左移 a << b
以位元左移赋值 a <<= b
位元右移 a >> b
以位元右移赋值 a >>= b
位元AND a & b
以位元AND赋值 a &= b
位元OR a | b
以位元OR赋值 a |= b
位元XOR a ^ b
以位元XOR赋值 a ^= b

其它运算符

运算符名称 语法
基本赋值 a = b
逗号 a , b

运算符(=)可以被用作赋值,意思是原定功能是由右方变量抄写内部资料到左方变量,但视乎需要也可以被用作其他用途。

每个运算符是互相独立存在,并不依赖其他运算符。例如运算符(<)并不需要运算符(>)存在从而运作。

一元可重载运算符

编辑

一元运算符跟上述的运算符相似,只是一元运算符只会加载类对象本身(*this),而不接受其他参数。另外,一元运算符有分前置运算符和后置运算符,分别在于前置运算符会放到变量前方,后置运算符则是后方。例如负值运算符(-)和逻辑取反运算符(!)都是一元前置运算符。

以下是一元可重载运算符列表:

算术运算符

运算符名称 语法 类型 备注
一元正号 +a 前置
前缀递增 ++a 前置 先加1并回传加1后的值
后缀递增 a++ 后置 加1但回传加1前的值
一元负号(取反) -a 前置
前缀递减 --a 前置 先减1并回传减1后的值
后缀递减 a-- 后置 减1但回传减1前的值

比较运算符

运算符名称 语法 类型
逻辑取反 !a 前置

位操作子

运算符名称 语法 类型
位元一的补码 ~a 前置

其它运算符

运算符名称 语法 类型
间接(向下参考) *a 前置
的地址(参考) &a 前置
转换 (type) a 前置

重载一元运算符时有区分前置和后置式,一元前置运算符按以下格式编写:

回傳資料型態 operator 運算符 ()

而后置运算符按以下格式编写:

回傳資料型態 operator 運算符 (參數)

括号重载

编辑

括号运算符有两种,分别是方形括号运算符([])和圆形括号运算符(())。方形括号运算符又名数组运算符,只能传入单一参数,而圆形括号运算符却可以传入任意数量的参数。

方形括号运算符按以下格式重载:

回傳資料型態 operator[] (參數)

圆形括号运算符按以下格式重载:

回傳資料型態 operator() (參數1, 參數2, ...)

注意,参数是指定在第二个括号之中,第一个括号只是运算符符号。

构造函数

编辑

有时软件工程师会想要他们的变量在声明时有一个默认值。这可以通过声明构造函数做到。

person(string N, int A) 
{
    name = N;
    age = A;
}

成员变量可以像下面的例子一样,利用一个冒号,通过一个初始化序列初始化。这与上面不同,它进行了初始化(使用构造函数),而不是使用赋值运算符。这对类类型来说更有效,因为它只需要直接构造;而赋值时,它们必须先使用默认构造函数进行第一次初始化,然后再赋予一个不同的值。而且一些类型(例如引用和const类型)不能被赋值,因而必须通过初始化序列进行初始化。

person(std::string N, int A) : name(N), age(A) {}

注意花括号不能被省略,即使为里面为空。

默认值可以给予最后的几个参数类帮助初始化默认值。

person(std::string N = "", int A = 0) : name(N), age(A) {}

在上面的例子中,当没有参数给予构造函数时,等价于调用以下的无参构造函数(一个默认构造函数):

person() : name(""), age(0) {}

构造函数的声明看起来像一个名字和数据类型相同的函数。事实上,我们的确可以用函数调用的形式调用构造函数。在这种情况下一个person类型的变量会成为返回值:

int main() 
{
    person r = person("Wales", 40);
    r.print();
}

以上的例子创建了一个临时的person对象,然后使用复制构造函数将其赋予r。一个更好的创建对象的方式(没有不需要的拷贝):

int main() 
{
    person r ("Wales", 40);
    r.print ();
}

具体的程序行为,可以也可以不和变量有关系,可以被作为一部分加入构造函数。

person() 
{
    std::cout << "Hello!" << endl;
}

通过以上的构造函数,当一个person变量没有被具体的值初始化时,“Hello!”会被打印。

默认构造函数

编辑

当类没有定义构造函数时,默认构造函数将被调用。

class A { int b;};
//使用括号创建对象
A *a = new A(); //调用默认构造函数,b会被初始化为'0'
//不使用括号创建对象
A *a = new A; //仅分配内存,不调用默认构造函数,b会有一个未知值

然而如果用户定义了这个类的构造函数,两个声明都会调用用户定义的构造函数。而在用户定义的构造函数中的代码会被执行,并且不会赋予b默认值。

析构函数

编辑

一个析构函数是一个构造函数的逆,当一个类的一个实例被销毁时会被调用,例如当一个类在块(一组花括号“{}”)中被构造的一个对象会在关闭括号后删除,之后析构函数被自动调用。它会在清空保存变量的内存位置时被调用。析构函数可以在类被销毁时用来释放资源,例如堆分配的内存和打开的文件。

声明一个析构函数的符号类似于构造函数。它没有返回值而且方法的名称和在类的名称前加上波浪线(~)相同。

~person() 
{
    cout << "I'm deleting " << name << " with age " << age << endl;
}

另外要注意的是,析构函数是不容许参数传递。然而,与构造函数一样,析构函数可以被显式调用:

int main() 
{
    person someone("Wales", 40);
    someone.~person();  //此時會輸出一次"I'm deleting Wales with age 40"

    return 0;  //第二次輸出"I'm deleting  with age 40"
}
/* 在這裡,程式結束時會自動調用析構函數,
   而person.name在第一次調用析構函數時已被清除,
   但person.age會按編譯器而定,
   沒能在第一次調用析構函數時清零。 */

构造函数与析构函数的相似点

编辑
  • 两者都和声明所在的类有相同的名字。
  • 若未宣告,两者都会执行默认的行为。意即类在建立或删除时,会一同分配或删除存储器。
  • 对派生类(subclass)而言,在基类(superclass)的构造函数执行期间,派生类的构造函数还未执行;反之,在基类的析构函数执行期间,派生类的析构函数已执行完毕。两种情况下,都无法使用在派生类宣告的变量。

类模板

编辑

类模板,是对一批仅仅成员数据类型不同的类的抽象,程序员只要为这一批类所组成的整个类家族创建一个类模板,给出一套程序代码,就可以用来生成多种具体的类,这类可以看作是类模板的实例,从而大大提高编程的效率。

属性

编辑

C++语法试图使一个结构的所有方面看起来像一个基本数据类型。因此,运算符重载允许结构像整数和浮点数一样操作,结构的数组可以通过方括号声明(some_structure variable_name[size]),而且指向结构的指针可以通过和指向内建类型的指针通用的方法解引用。

内存消耗

编辑

结构的内存消耗至少是组成变量的内存大小的总和。参考如下twonums 结构例子。

struct twonums 
{
    int a;
    int b;
};

这个结构包含两个整型。在当前许多 C++ 编译器中,整型默认32 位整型, 所以每个成员变量消耗 4 个字节的内存.因而整个结构至少(或者正好)消耗 8 个字节的内存,见下图。

+----+----+
| a  | b  |
+----+----+

然而,编译器可能在变量或者结构的结尾添加空的位, 这样可以保证和给定的电脑结构匹配,通常是把变量添加到 32 位。如下例子所示的结构:

struct bytes_and_such
{ 
   char c;
   char C;
   short int s;
   int i;
   double d;
};

可看成

+-+-+--+--+--+--+--------+
|c|C|XX|s |  i  |   d    |
+-+-+--+--+--+--+--------+

在内存中, XX 表示两个未被使用的空位元。

因为结构可能会使用指针和数组去声明 或者初始化变量,结构的内存消耗不一定是固定的。另外一个内存消耗不固定的例子是模板结构。

位字段

编辑

位字段(Bit field)可以被用来定义比内建类型还要小的类成员变量。通过这个字段定义的变量,只可以像使用内建的整数类型(例如int, char, short, long...)那样子使用。

struct A
{ 
	unsigned a:2; // 可以存储0-3的数字,占据一个int前2 bit的空间.
	unsigned b:3; // 可以存储0-7的数字,占据之后3 bit的空间.
	unsigned :0;  // 移动到下一个内置类型的末尾
	unsigned c:2; 
	unsigned :4;  // 在c和d中间加4bit的空白
	unsigned d:1;
	unsigned e:3;
};

// 内存结构
/*	4 byte int   4 byte int
	[1][2][3][4] [5][6][7][8]
	[1]                   [2]              [3]              [4]
	[a][a][b][b][b][][][] [][][][][][][][] [][][][][][][][] [][][][][][][][]

	[5]                  [6]                [7]              [8]
	[c][c][][][][][d][e] [e][e][][][][][][] [][][][][][][][] [][][][][][][][]
*/

位字段不能在结构体中使用,它只能在使用struct或者class关键字定义的类中使用。

按引用传参

编辑

this关键字

编辑
complex& operator+=(const complex & c) 
{
    realPart += c.realPart;
    imagPart += c.imagPart;
    return *this;
}

参见

编辑

参考

编辑
  1. ^ 1.0 1.1 ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §9 Classes [class] para. 4
  2. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §8.5.1 Aggregates [dcl.init.aggr] para. 1
  3. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §8.5.1 Aggregates [dcl.init.aggr] para. 2
  4. ^ What's this "POD" thing in C++ I keep hearing about?. Comeau Computing. [2009-01-20]. (原始内容存档于2009-01-19). 
  5. ^ Henricson, Mats; Nyquist, Erik. Industrial Strength C++. Prentice Hall. 1997. ISBN 0-13-120965-5. 
  6. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §9.2 Class members [class.mem] para. 12
  7. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §9.2 Class members [class.mem] para. 14
  8. ^ 8.0 8.1 ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §9.2 Class members [class.mem] para. 17
  9. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §18.1 Types [lib.support.types] para. 5
  10. ^ thiscall (C++). [2009-01-26]. (原始内容存档于2008-12-10). 
  11. ^ integer *k不同,使用符号(*)只会传递变量地址,而且只能利用变量地址操作变量本身。

资料来源

编辑