基本上是对《C++ Primer》的摘抄
想理解类,需要先从数据结构开始理解,所谓数据结构,是把一组相关的数据元素组织起来然后使用它们的策略和方法。
从一个简单类开始:
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
该类由struct关键字开始(这里觉得自己以前理解太片面了,只想着class关键字就是类,实则不然),类体由花括号包括形成了一个新的作用域,类内部名字唯一,但是可以与类外定义的名字重复。
不建议把对象的定义和类的定义放在一起的行为:
struct Sales_data {/*...*/} accum, trans, *salesptr;
类体定义类的成员,上述类只有数据成员,数据成员定义了类的对象的具体内容,每个对象有自己的一份数据成员拷贝,即修改一个对象的数据成员,不会影响其他的Sales_data对象。
C++11新标准,可以给数据成员一个类内初始值,没有初始值的成员将被默认初始化。
上述内容作为类一部分的引入。
类的基本思想是数据抽象和封装。数据抽象是种依赖于接口和实现分离的编程(以及设计)技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体及定义类所需的各种私有函数。
封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。
类想要实现数据抽象和封装,需要首先定义一个抽象数据类型(ADT)。在抽象数据类型中,由类的设计者负责考虑类的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么,而无需了解类型的工作细节。
上述Sales_data类并不是一个抽象数据类型。它允许类的用户直接访问它的数据成员,并且要求用户编写操作。
struct Sales_data {
//关于Sales_data对象的操作
std::string isbn() const {return bookNo;}
Sales_data& combine(const Sales_data&);
double avg_price() const;
//数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
//非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
定义在类内部的函数是隐式的inline函数
引入this
思考isbn()是如何获得bookNo成员所依赖的对象,观察对该函数的调用:
Sales_data total;
total.isbn();
使用了点运算符,当我们调用成员函数时,实际上是在替某个对象调用它,如果isbn指向Sales_data的成员(如bookNo),则它隐式地指向调用该函数的对象的成员,即isbn返回bookNo时,实际上它隐式地返回total.bookNo。
成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this,如:调用total.isbn()
编译器负责把total的地址传递给isbn的隐式形参this,可以等价地认为编译器将该调用写成了如下形式:
Sales_data::isbn(&total) //伪代码
//调用Sales_data的isbn成员时传入了total的地址
在成员函数内部,可以直接调用该函数的对象的成员,因为this所指的正是这个对象,任何对类成员的直接访问都被看作this的隐式引用,isbn使用bookNo时,实际上相当于使用了this->bookNo。
由于this的目的总是指向“这个”对象,所以this是一个常量指针。
再次观察isbn函数,观察其const关键字:
std::string isbn() const { return bookNo; }
这里const的作用是修改隐式this指针的类型。
默认情况下,this的类型是指向类类型非常量版本的常量指针。相当于Sales_data *const。我们不能把this绑定到一个常量对象上,使得我们不能在一个常量对象上调用普通的成员函数。
如果isbn是一个普通函数而且this是一个普通的指针参数,我们应该把this声明成const Sales_data *const。然而this不会出现在参数列表中,所以C++规定把const关键字放在参数列表后面表示this是一个指向常量的指针,这样的函数成为常量成员函数

类作用域和成员函数
注意类本身就是一个作用域,类的成员函数定义嵌套在类的作用域之内,注意到即使bookNo定义在isbn之后,还是能正常使用,因为编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体。
在类的外部定义成员函数
保持与声明的一致,另外注意成员的名字必须包含它所属的类名:
double Sales_data::avg_price() const {
if(units_sold)
return revenue/units_sold;
else
return 0;
}
定义一个返回this对象的函数
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold; //把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this; //返回调用该函数的对象
}

定义类相关的非成员函数

初窥构造函数
每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫构造函数。
任务:初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
构造函数的名字和类名相同。但是没有返回类型,包含一个可能为空的参数列表和一个可能为空的函数体。类可以包括多个构造函数,参考重载函数。
注意构造函数不能被声明为const的,当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其常量属性。因此构造函数在const对象的构造过程中可以向其写值。
合成的默认构造函数
之前的Sales_data类没有定义构造函数,仍可以正常运行:
Sales_data total;
Sales_data trans;
执行默认初始化,类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。无需任何实参。
如果类没有显示地定义构造函数,编译器就会为我们隐式地定义一个默认构造函数。
编译器创建的构造函数又被称为合成的默认构造函数。其初始化类的数据成员的规则:
1.如果存在类内的初始值,用它来初始化成员。
2.否则,默认初始化该成员。

struct Sales_data{
Sales_data() = default;
Sales_data(const std::string &s): bookNo(s) { } //构造函数初始化值列表
Sales_data(const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) { }
Sales_data(std::istream &);
}
=default


拷贝、赋值和析构

本文深入探讨C++中的类,从数据结构的角度解释类的概念,通过实例展示如何定义和使用类。类是数据抽象和封装的基础,提供了一种隐藏实现细节并仅公开接口的方式。文章讲解了成员函数、构造函数、拷贝和赋值行为,以及默认构造函数的作用。此外,还讨论了const成员函数和非成员函数在类设计中的重要性。
1021

被折叠的 条评论
为什么被折叠?



