【cpluscplus教程翻译】Classes (I)

类(Class)

类是struct的扩展概念:就像struct一样,类可以包含数据成员,也可以包含函数成员。
对象(object)是类的实例,参考变量的概念,类就相当于类型,对象相当于变量。
可以通过关键字class或struct定义类(区别在于默认的访问权限,目的是保持c++兼容c,所以class默认private,struct默认public),语法如下

class class_name {
  access_specifier_1:
    member1;
  access_specifier_2:
    member2;
  ...
} object_names;

class_name是一个有效的标识符即可,object_names可选,声明体包含的称为成员(members),既可以是数据也可以是函数声明,访问权限(access specifiers)可选。
相比数据结构,类增加了函数和访问权限,访问权限使用三个关键字:private,public和protected:
private成员只可以由同一个类或类的friends的其它成员访问,protected在private基础上,还可以由派生类的成员访问,只要对象可见,public成员就可以访问(可以参考可见性,class和命名空间内部有相通性,想想::操作符用在哪里
class默认使用private,在任何访问权限之前声明的成员自动是private,例如

class Rectangle {
    int width, height;
  public:
    void set_values (int,int);
    int area (void);
} rect;

声明类型Rectangle及一个该类型的对象rect,类包括四个成员:两个int型数据成员(width height),访问权限是private,两个函数成员(set_values area),访问权限是public,注意的是这段代码只有声明,没有定义。
请注意类名和对象名的不同:Rectangle是类型名,rect是变量名
public的成员访问非常简单:

rect.set_values (3,4);
myarea = rect.area();

width和height在类外(namespace)是不能被访问的,因为他们的访问权限是private,只能在类的其他成员中被访问,Rectangle更完整的声明如下

// classes example
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    void set_values (int,int);
    int area() {return width*height;}
};

void Rectangle::set_values (int x, int y) {
  width = x;
  height = y;
}

int main () {
  Rectangle rect;
  rect.set_values (3,4);
  cout << "area: " << rect.area();
  return 0;
}

这个例子再次引入了作用域操作符(::),注意与namespaces的联系,这里作用域操作符被用在类外进行函数定义。
area的函数定义在类内部,因为这个函数非常简单,set_values只在类内声明了原型,定义在外面,在外面的定义中,作用域操作符用来说明这个函数是类的成员函数,而不是一个一般的非成员函数。
作用域操作符指定了成员属于哪个类,这就保证了同样的作用域属性,就好像这个函数定义直接放在类里。set_values虽然定义在类外,但是可以访问width,height,这两个变量是private属性(作用域操作符把类的可见性引入了,就好像using
类内类外定义函数的唯一不同:类内会被编译器自动认为是内联函数(inline),类外只是普通函数,行为上没什么不同(能够真的内联要看函数复杂度和编译器的决定)。
width和height是private权限,类外访问是不允许的,这很好理解,因为我们已经定义了一个成员函数来设置这两个变量的值set_values,因此剩下的程序没必要访问这些变量:在这么简单的一个例子里,可能看不出如此严格进行访问限制有什么作用,但是在大型程序中,避免值被意料之外的方式进行修改是非常重要的。
类最重要的属性就是它是一个类型,可以用来声明多个对象,例子如下

// example: one class, two objects
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    void set_values (int,int);
    int area () {return width*height;}
};

void Rectangle::set_values (int x, int y) {
  width = x;
  height = y;
}

int main () {
  Rectangle rect, rectb;
  rect.set_values (3,4);
  rectb.set_values (5,6);
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}

在这个例子中,声明了两个对象,每个对象都有自己的成员变量和成员函数(函数指针还是确实多了一份代码段?和对象内存模型有关
rect.area()和rectb.area()的函数返回值并不一样,这是因为每个对象都有自己的变量width和height,同样的道理,每个对象也有自己的成员函数set_values和area操作在他们自己的成员变量上。
类允许OOP(面向对象编程):数据和函数是对象的成员,因此不用进行参数传递,因为这些数据是对象的一部分,注意到area的函数不用传参就可以访问到height和width。

构造函数(Constructors)

在前面这个例子中,如果在调用set_values之前就调用area会发生什么,一个不确定的结果将会发生,因为width和height还未被初始化(注意和命名空间的不同,命名空间的变量会被初始化为0,很好理解,因为成员变量是要去实例化的,此时还没有内存
为了避免这种情况,类需要一个特殊的函数:构造函数,这个函数会在新生成对象时自动调用,这样类就可以初始化成员变量或分配内存。
构造函数的声明和普通函数没啥区别,只不过名字必须是类型名,且没有返回值(避免产生歧义),示例如下

// example: class constructor
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle (int,int);
    int area () {return (width*height);}
};

Rectangle::Rectangle (int a, int b) {
  width = a;
  height = b;
}

int main () {
  Rectangle rect (3,4);
  Rectangle rectb (5,6);
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}

这个例子和之前差不多,但是没有成员函数set_values,替代它的是一个构造函数,作用差不多。
请注意在生成对象时,参数是怎样传递给构造函数的

Rectangle rect (3,4);
Rectangle rectb (5,6);

不同于普通的成员函数,构造函数不能被显式调用,它只能被在对象生成时执行一次(可以认为是编译器操作的,new操作符也是这个原理)
注意到构造函数的原型声明和定义都没有返回值,顾名思义,构造函数只初始化对象

重载构造函数

和其他函数一样,构造函数也可以重载:有不同的版本,每个版本的参数类型和个数可以不一样,编译器会自动选择合适的(注意int long之间的参数提升

// overloading class constructors
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle ();
    Rectangle (int,int);
    int area (void) {return (width*height);}
};

Rectangle::Rectangle () {
  width = 5;
  height = 5;
}

Rectangle::Rectangle (int a, int b) {
  width = a;
  height = b;
}

int main () {
  Rectangle rect (3,4);
  Rectangle rectb;
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}

这个例子还引入了默认构造函数,默认构造函数虽然没有参数,但是不要加圆括号(如果有一个函数没有参数,返回这个类型,编译器会不知道调用哪个函数,也为了兼容C语言?

Rectangle rectb;   // ok, default constructor called
Rectangle rectc(); // oops, default constructor NOT called 

统一初始化(Uniform initialization)

上面这种形式:用圆括号包括参数调用构造函数,被称为函数形式(functional form),但是构造函数还可以通过别的语法调用:
首先,只有一个参数的构造函数可以直接用赋值运算符调用
class_name object_name = initialization_value;
最新的C++引入了统一初始化,本质上和函数形式差不多,只不过用花括号替代圆括号class_name object_name { value, value, value, ... },花括号之前可选增加赋值运算符。
四种例子如下:

// classes and uniform initialization
#include <iostream>
using namespace std;

class Circle {
    double radius;
  public:
    Circle(double r) { radius = r; }
    double circum() {return 2*radius*3.14159265;}
};

int main () {
  Circle foo (10.0);   // functional form
  Circle bar = 20.0;   // assignment init.
  Circle baz {30.0};   // uniform init.
  Circle qux = {40.0}; // POD-like

  cout << "foo's circumference: " << foo.circum() << '\n';
  return 0;
}

统一初始化的优点之一是圆括号没办法调用默认构造函数,花括号可以

Rectangle rectb;   // default constructor called
Rectangle rectc(); // function declaration (default constructor NOT called)
Rectangle rectd{}; // default constructor called 

选择哪种语法主要是风格的影响,大部分现存的代码用函数式,一些新的风格指南推荐用统一初始化,统一初始化会有一些隐藏问题,如果参数类型是初始化列表

构造函数里的成员初始化

构造函数初始化数据成员可以不用函数体

class Rectangle {
    int width,height;
  public:
    Rectangle(int,int);
    int area() {return width*height;}
};

构造函数的一般形式为
Rectangle::Rectangle (int x, int y) { width=x; height=y; },也可以改成成员初始化形式Rectangle::Rectangle (int x, int y) : width(x) { height=y; }或者Rectangle::Rectangle (int x, int y) : width(x), height(y) { },最后这种形式构造函数没有函数体
对于属于基础数据类型的成员,上面这种方法和赋值运算符没有什么区别,但是对于class成员,如果它没有在冒号后初始化,他们将被默认构造
默认构造有些时候可能会不够方便:有些情况还是一种浪费(成员变量稍后又会被重新初始化),但是有些情况下,默认构造函数可能是不可能的(如果类没有默认构造函数),在这些情况下,成员需要用初始化列表

// member initialization
#include <iostream>
using namespace std;

class Circle {
    double radius;
  public:
    Circle(double r) : radius(r) { }
    double area() {return radius*radius*3.14159265;}
};

class Cylinder {
    Circle base;
    double height;
  public:
    Cylinder(double r, double h) : base (r), height(h) {}
    double volume() {return base.area() * height;}
};

int main () {
  Cylinder foo (10,20);

  cout << "foo's volume: " << foo.volume() << '\n';
  return 0;
}

Cylinder有两个成员变量,有一个类型也是类Circle,由于Circle只能用一个参数构造,因此Cylinder的构造函数需要调用Circle的构造函数(如果想把circle的初始化挪到函数体里,要写赋值语句,但是circle的声明已经在class里写了,所以会有问题),所以必须用member initializer list
初始化也可以用统一初始化形式Cylinder::Cylinder (double r, double h) : base{r}, height{h} { }

类的指针

对象也可以用指针指向,只要声明了,class就是一个有效的类型,所以也可以用来修饰指针
Rectangle * prect;
和struct一样,用箭头运算符就可以访问指针指向的对象的成员,例子如下

// pointer to classes example
#include <iostream>
using namespace std;

class Rectangle {
  int width, height;
public:
  Rectangle(int x, int y) : width(x), height(y) {}
  int area(void) { return width * height; }
};


int main() {
  Rectangle obj (3, 4);
  Rectangle * foo, * bar, * baz;
  foo = &obj;
  bar = new Rectangle (5, 6);
  baz = new Rectangle[2] { {2,5}, {3,6} };
  cout << "obj's area: " << obj.area() << '\n';
  cout << "*foo's area: " << foo->area() << '\n';
  cout << "*bar's area: " << bar->area() << '\n';
  cout << "baz[0]'s area:" << baz[0].area() << '\n';
  cout << "baz[1]'s area:" << baz[1].area() << '\n';       
  delete bar;
  delete[] baz;
  return 0;
}

这个例子用了解引用、取地址、.、箭头、下标等操作符

struct和union

类不仅可以用关键字class定义,还可以用关键字struct和union定义。
关键字struct通常用于声明纯数据结构,也可以用于声明具有成员函数的类,语法与关键字class相同。两者之间唯一的区别是,默认情况下,用关键字struct声明的类的成员具有公共访问权限,而默认情况下用关键字class声明的类成员具有私有访问权限。对于其他所有情况,这两个关键字在本文中是等效的。
相反,并集的概念与用结构和类声明的类的概念不同,因为并集一次只存储一个数据成员,但它们也是类,因此也可以保存成员函数。联合类中的默认访问权限是公共的。

我的理解

C的最小编译单元可以认为是一个.c文件,控制力度太粗,容易发生各种名字冲突,static的作用不是太大,C++在此基础上引入namespace,在全局可见性和局部可见性之间又加了一层,且namespace是编译期处理,可以跨文件,然后又在这个基础上提出了class的概念,所以class外部的声明用的是和命名空间一样的运算符。
初始化列表在设计上是十分必要的,本质上构造函数实际上是在任何函数体外运行一次的

posted @ 2023-05-19 15:07  xiaoweing  阅读(40)  评论(0)    收藏  举报