C++ 类与构造函数

抽象

对象大致意味着一系列数据(属性)以及一套访问和操作这些数据的方法。使用对象而非全局变量和函数的原因有多个,下面列出了使用对象的最重要的好处。

  • 多态:可对不同类型的对象执行相同的操作,而这些操作就像“被施了魔法”一样能够正常运行。
  • 封装:对外部隐藏有关对象工作原理的细节。
  • 继承:可基于通用类创建出专用类。
isinstance(object, tuple)

这里使用isinstance来执行类型/类检查旨在说明:使用类型检查通常是馊主意,应尽可能避免;

简介

类是数据结构的扩展概念:与数据结构一样,它们可以包含数据成员,但它们也可以包含作为成员的函数。

一个对象是一个类的实例化。就变量而言,类是类型,对象是变量。

使用关键字class或关键字定义类struct,使用以下语法:

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

class_name是类的有效标识符,object_names是此类对象的可选名称列表。声明的主体可以包含成员,可以是数据或函数声明,也可以是访问说明符。

类具有与普通数据结构相同的格式,除了它们还可以包含函数并具有称为访问说明符的这些新东西。一个访问说明符是以下三个关键字之一:privatepublicprotected。这些说明符修改了跟随它们的成员的访问权限:

  • private类成员只能从同一个类的其他成员(或他们的“朋友”)中访问。
  • protected成员可以从同一类的其他成员(或来自他们的“朋友”)访问,也可以从其派生类的成员访问。
  • 最后,public可以从对象可见的任何位置访问成员。

默认情况下,使用class关键字声明的类的所有成员都对其所有成员具有私有访问权限。因此,在任何其他访问说明符之前声明的任何成员都会自动拥有私有访问权限。例如:

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

声明一个被调用的类(即一个类型)Rectangle和一个被称为该类的对象(即一个变量)rect。这个类包含四个成员:类型的两个数据成员int(成员width和成员height)与private(因为private是默认的访问级别)和public的两个成员函数。函数set_valuesarea,现在只有他们的声明,没有他们的定义。

注意类名和对象名之间的区别:在前面的例子中,Rectangle是类名(即类型),而是rect类型的对象Rectangle。它是相同的关系inta并在以下声明中:

int a;

int是类型名称(类),a是变量名称(对象)。

声明之后Rectanglerect,任何对象的公共成员rect可以如同它们是正常功能或正常变量进行访问,通过简单地插入一个.在对象名称和成员名称之间。这遵循与访问纯数据结构成员相同的语法。例如:

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

唯一的成员rect不能从类的外部访问是widthheight,因为他们有private,他们只能从同一类的其他成员中提到。

// 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;
}

此示例重新引入了scope operator::,在前面的章节中与名称空间相关。这里它用于函数的定义,set_values以定义类本身之外的类的成员。

set_values它仅在类中声明其原型,但其定义不在其中。在此外部定义中,::用于指定要定义的函数是类Rectangle的成员而不是常规的非成员函数。

::指定要定义的成员所属的类,授予完全相同的作用域属性,就好像此函数定义直接包含在类定义中一样。例如,该功能set_values在前面的例子中可以访问private的变量widthheight

在类定义中完全定义成员函数或仅在函数中包含其声明并在稍后在类外定义它的唯一区别是:

  • 在第一种情况下,函数自动被视为内联函数成员函数由编译器;
  • 而在第二个它是一个普通(非内联)类成员函数。这不会导致行为差异,但仅限于可能的编译器优化。

成员widthheight具有私有访问权限(请记住,如果未指定任何其他内容,则使用关键字定义的类的所有成员class都具有私有访问权限)。通过声明它们是私有的,不允许从类外访问。因为我们已经定义了一个成员函数来为对象中的那些成员设置值:成员函数set_values。因此,程序的其余部分不需要直接访问它们。也许在这样一个如此简单的例子中,很难看出对这些变量的限制访问是如何有用的,但在更大的项目中,不能以意想不到的方式修改值可能是非常重要的(从意图的角度来看是意外的)物体)。

构造函数-Constructors

如果我们在调用area之前调用成员函数,前一个例子会发生什么set_values?一个未确定的结果,因为成员width``height从未被赋予过值。

为了避免这种情况,类可以包含一个称为其构造函数的特殊函数,只要创建此类的新对象,就会自动调用该函数,从而允许类初始化成员变量或分配存储。

这个构造函数声明就像一个普通的成员函数,但其名称与类名相匹配,没有任何返回类型; 不是void。通过实现构造函数可以很容易地改进上面的Rectangle类:

// 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;
}

该示例的结果与前一示例的结果相同。但是现在,class Rectangle没有成员函数set_values,而是有一个执行类似操作的构造函数:它初始化传递给它的参数的值widthheight

注意这些参数在创建此类的对象时如何传递给构造函数:

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

无法明确调用构造函数,就好像它们是常规成员函数一样。当创建该类的新对象时,它们仅执行一次。

⚠️构造函数原型声明(在类中)和构造函数的定义返回值不是void,而是没有返回值。构造函数永远不会返回值,它们只是初始化对象。

重载构造函数

与任何其他函数一样,构造函数也可以使用具有不同参数的不同版本进行重载:具有不同数量的参数和/或不同类型的参数。编译器将自动调用其参数与参数匹配的那个:

// 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;
}

在上面的例子中,Rectangle构造了两个类对象:rectrectbrect用两个参数构造,就像之前的例子一样。

但是这个例子还引入了一种特殊的构造函数:默认构造函数。该默认构造函数是不带参数的构造,是当一个对象被声明,但不使用任何参数进行初始化的叫法。在上面的示例中,调用默认构造函数rectb。注意rectb甚至没有用空的括号构造 - 事实上,空括号不能用于调用默认构造函数:

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

这是因为空的括号集将构成rectc函数声明而不是对象声明:它将是一个不带参数并返回某个类型值的函数Rectangle

统一初始化 uniform init

如上所示,通过将其参数括在括号中来调用构造函数的方式称为函数形式的初始化(function form)。但是也可以使用其他语法调用构造函数:首先,可以使用变量初始化语法(即等号后跟参数)调用具有单个参数的构造函数:

class_name object_name = initialization_value; 

最近,C++引入了使用统一初始化调用构造函数的可能性,它基本上与函数形式相同,但使用大括号{}而不是括号()

class_name object_name { value, value, value, ... } 

最后一个语法形式上同上,但需要在大括号之前包含等号。

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 

调用构造函数的语法选择很大程度上取决于样式。现有的大多数代码都使用函数形式,一些较新的样式指南建议选择统一的初始化,而不是其他的,尽管它还存在一些潜在的缺陷,比如首选initializer_list作为其类型。

构造函数中的成员初始化

当构造函数用于初始化其他成员时,可以直接初始化这些其他成员,而无需在其函数体内做任何事使用语句。这是通过在构造函数体之前插入冒号(:)和类成员的初始化列表来完成的。例如,考虑具有以下声明的类:

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) { }

⚠️在最后一种情况下,构造函数除了初始化其成员之外什么都不做,因此它有一个空函数体。

对于基本类型的成员,上面定义的构造函数是没有区别的,因为默认情况下它们没有初始化,但对于成员对象(类型为类的那些),如果它们在冒号后没有初始化,它们是默认构造的。

类中的所有成员其默认构建可能方便,可能也不方便:在某些情况下,甚至是浪费(当成员变量在构造函数中重新初始化时),但在某些其他情况下,默认构造甚至是不可能的(当类没有默认构造函数时)。在这些情况下,成员变量应在成员初始化列表中初始化。例如:

// 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;
}

在这个例子中,class Cylinder有一个成员对象,其类型是另一个类(base的类型是Circle)。因为类的对象Circle只能用参数构造,所以Cylinder构造函数需要调用base构造函数,唯一的方法是在成员初始化列表中。

这些初始化也可以使用统一的初始化语法,使用大括号{}而不是括号()

Cylinder::Cylinder (double r, double h) : base{r}, height{h} { }

类指针

对象也可以通过指针指向:一旦声明,类就成为有效类型,因此它可以用作指针指向的类型。例如:

Rectangle * prect;

是指向类对象的指针Rectangle

与纯数据结构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,还可以使用关键字structunion定义。

通常用于声明普通数据结构的关键字struct也可用于声明具有成员函数的类,其语法与关键字class声明相似。两者之间的唯一区别是,使用关键字struct声明的类成员默认具有public访问权限,而使用该关键字class声明的类成员默认具有private访问权限。对于所有其他目的,两个关键字在此上下文中是等效的。

关键字union的声明与关键字structclass不同,因为union一次只存储一个数据成员,但是它们也是类,因此也可以保存成员函数。union类中的默认访问权限是public

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,727评论 2 9
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,500评论 1 51
  • C++文件 例:从文件income. in中读入收入直到文件结束,并将收入和税金输出到文件tax. out。 检查...
    SeanC52111阅读 2,751评论 0 3
  • 赋值同步赋值:变量1,变量2,....变量n=表达式1,表达式2,.....表达式n同步赋值首先计算右边n个表达式...
    知识学者阅读 408评论 2 2
  • 《雨中寻觅》 作者:王保帅 雨沙沙绵绵的漂洒 我们在雨中寻觅 雨朦胧 诗胧美 歌声在雨中荡漾 目标在雨中隐约显现 ...
    仁厚道阅读 300评论 0 2