文章目录
前言
从这一章开始开始上面向对象的内容,在这一章里将介绍面向对象的编程,抽象和类,类的构造函数和析构函数,this指针,对象数组,类作用域等等。因为类在C++中非常重要,我决定还是写成一个系列方便后面复习吧,这是这个系列的第一部分。
面向对象的编程
面向对象程序设计(Object Oriented Programming,OOP)是一种计算机编程架构。OOP的一条基本原则是计算机程序由单个能够起到子程序作用的单元或对象组合而成。OOP达到了软件工程的三个主要目标:重用性、灵活性和扩展性。OOP=对象+类+继承+多态+消息,其中核心概念是类和对象。
C++就是这样的一种面向对象的编程语言,而C语言则是面向过程的一种编程语言,即“面向过程”(Procedure Oriented)是一种以过程为中心的编程思想。这些都是以什么正在发生为主要目标进行编程,不同于面向对象的是谁在受影响。
举个例子理解他们的区别:
假如要乘出租车去机场。
面向过程的方式是:告诉司机,按照
启动、右转、左转、停止
等单独的接口去机场。这种方式需要乘客对自己的行为负责,乘客需要知道每个城市去机场的路线。面向对象的方式是:告诉司机,
请载我去机场
。显然这种方法比面向过程的方法要容易得多。在面向过程编程时,由于主程序承担的责任太多,要确保一切正确工作,还要协调各个函数并控制它们的先后顺序,因此经常会产生非常复杂的代码。很多时候变化是不可避免的,而功能分解法(面向过程思想)却又无法应对可能出现的变化。一旦修改代码,则bug越来越多。
因此,面对复杂的软件开发时,主程序不能做太多的事情,必须通过
分离关注点
进行职责转移。在上面的例子中,尽管具体实现在广州、北京或上海等不同城市中是不同的,但在任何城市都可以这么说,因为司机知道怎么去机场,这就是职责转移
。
面向对象开发具有这些特性:封装和数据隐藏、抽象、继承、多态、代码的可重用性
抽象和类
类的声明
类是 C++ 的核心特性,通常被称为用户定义的类型。类用于指定对象的形式,它包含了数据表示法和用于处理数据的方法。
类中的数据和方法称为类的成员。函数在一个类中被称为类的成员函数。
在介绍类之前,我们先来介绍接口,接口是一个共享框架,供两个系统(如在计算机和打印机之间或者用户或计算机程序之间) 交互时使用;例如,我们不能直接将脑子中想到的词传给到计算机内存中,而必须使用程序提供的接口交互。程序接口将你的意图转换为存储在计算机中的具体信息。
对于类,我们有公共接口和私有接口。在这里,公众(public) 是使用类的程序,交互系统由类对象组成,而接口由编写类的人提供的方法组成。而私有(private)则是类中不能被交互的一部分,需要通过公共接口来调用。要编写类,必须创建其公共接口。(才能交互)
公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。防止程序直接访问数据被称为数据隐藏。
通常, 程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。
我们来看一个类的声明的例子,在stock00.h文件中:
#ifndef STOCK00_H_
#define STOCK00_H_
#include <string>
class Stock // class declaration
{
long shares; //private by default
private:
std::string company;
double share_val;
double total_val;
void set_tot() { total_val = shares * share_val; }
public:
void acquire(const std::string & co, long n, double pr);
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
}; // note semicolon at the end
#endif
- 在这个例子中,使用到了class关键字声明一个类,其中还用到了private、public关键字表示私有和公有成员的声明。C++还有一个protected控制关键字,在这一章不会提及,之后会提及。
- 因为是在头文件中的声明,故只需写声明即可,不用写定义。在private里的set_tot()函数十分简单,在使用的时候自动成为内联函数。
- 无论类成员是数据成员还是成员函数,都可以在类的公有部分或私有部分中声明它。但由于隐藏数据是OOP 主要的目标之一,因此数据项通常放在私有部分,组成类接口的成员函数放在公有部分。
- 其实不必在类声明中使用关键字private, 因为这是类对象的默认访问控制。如shares变量就默认为私有。
我们看完第四点也许会想到结构,结构的定义是如此的,例如:
struct Student{
char name[81]; //public by default
int id; //public by default
}
我们发现结构中定义的数据和函数都是可以直接使用的,那就说明结构的默认访问类型是public(C++中拓展的特性)。C++程序员通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象。
声明中的成员函数
我们创建了一个类,定义好了数据成员,更多的需要成员函数来实现某些功能、访问私有成员等,接下来就来介绍成员函数。
成员函数定义与常规函数定义非常相似, 它们有函数头和函数体,也可以有返回类型和参数。但是它们还有两个特殊的特征:
- 定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类;
- 类方法可以访问类的private 组件。
这两个性质都很重要,我们先来看第一个。例如我们在之前的头文件中声明的类,其公有成员函数void update(double price);
,函数头可以写成:void Stock::update(double price);
这么做就说明我们可以在不同的类中声明相同的名字,例如同时声明void Buffer::update(double price);
这样不会产生冲突。(这不是函数使用)
因此,作用域解析运算符确定了方法定义对应的类的身份。我们说,标识符update( )具有类作用域(class scope)。在一个成员函数中,Stock类的其他成员函数不必使用作用域解析运算符,就可以使用update()方法,这是因为它们属于同一个类,因此update( )是可见的。
例如:
void Stock::show()
{
std::cout << "Company: " << company
<< " Shares: " << shares << '\n'
<< " Share Price: $" << share_val
<< " Total Worth: $" << total_val << '\n';
}
这种函数定义方式与名称空间的函数定义很像,例如:
void Names::show(){...}
void Stock::show(){...}
对象与使用类
我们创建了一个类方法,但我们实现交互,需要一个对象,我们通过声明类变量来创建对象。
例如:Stock kate,joe;
这将创建两个Stock 类对象, 一个为kate, 另一个为joe。
使用对象的成员函数,和使用结构成员一样,通过成员运算符(.):
kate.show();//对象kate调用函数show()
joe.show();//对象joe调用函数show()
所创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法,即每种方法只有一个副本。简单来说:方法是一样的,执行的对象不同、数据不同罢了。
知道如何定义类及其方法后,来创建一个程序,它创建并使用类对象。C++的目标是使得使用类与使用基本的内置类型(如int和char)尽可能相同。要创建类对象,可以声明类变量,也可以使用new为类对象分配存储空间。可以将对象作为函数的参数和返回值,也可以将一个对象赋给另一个。
例如我们把这个文件和头文件以及定义文件同时编译:
#include <iostream>
#include "stock00.h"
int main()
{
Stock test;
test.show();
return 0;
}
以上就是基础的类的声明与定义。总结一下,类的使用分为以下三个步骤:
- 类声明类似结构声明,可以包括数据成员和函数成员。声明有私有部分,在其中声明的成员只能通过成员函数进行访问;声明还具有公有部分,在其中声明的成员可被使用类对象的程序直接访问。通常,数据成员被放在私有部分中,成员函数被放在公有部分中。模板可以写成:
class classname { private: /* data */ public: function(/* args */); /* public function */ };
- 声明完以后需要定义函数,可以在类声明中提供完整的函数定义,而不是函数原型,但是通常的做法是单独提供函数定义(除非函数很小)。模板可以写成:
classname::function(/* args */) {}
- 使用类时可通过类对象来调用。创建类变量即对象,使用成员指示符(.)访问类对象下的数据与函数,模板为:
classname obj; obj.function();
类的构造函数和析构函数
构造函数
我们定义结构的时候,可以直接对其初始化,例如:
struct person{
std::string Name;
int age;
};
person stu = {"Jack",18};//valid initialization
这样初始化没有任何问题,但是在类中就不能如此。数据部分的访问状态是私有的,程序不能直接访问数据成员。我们只能通过成员函数来访问数据成员,因此需要设计合适的成员函数,才能成功地将对象初始化(如果使数据成员成为公有,而不是私有,就可以按刚才介绍的方法初始化类对象,但使数据成为公有的违背了类的一个主要初衷:数据隐藏)
C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值赋给它们的数据成员,自动对类数据进行初始化。这个函数它的名称与类名相同!它还有一个特别的地方——构造函数没有声明类型,即没有返回值,也没有被声明为void类型。
前面的类声明有点远,我们再写一遍:
#include <string>
class Stock // class declaration
{
private:
std::string company;
double share_val;
double total_val;
long shares;
void set_tot() { total_val = shares * share_val; }
public:
void acquire(const std::string & co, long n, double pr);
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
};
我们看私有数据那里,我们想一开始就初始化的值应该是company、share_val和shares数据。我们设置company,其他保持缺省,函数原型可以这么写:
Stock(const string & co, long n=0 , double pr=0.0);
第一个参数是指向字符串的指针,该字符串用于初始化成员company。n和pr参数为shares和share_val成员提供值。注意,没有返回类型。原型位于类声明的公有部分。
还需要注意的是:
- 程序声明对象时,将自动调用构造函数。
- 不能将类成员名称用作构造函数的参数名。
- 构造函数被用来创建对象,而不能通过对象来调用。
其定义我们可以自己决定,例如:
Stock::Stock(const std::string & co, long n, double pr)
{
company = co;
if (n < 0)
{
std::cout << "Number of shares can't be negative; "
<< company << " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
C++提供了两种使用构造函数来初始化对象的方式。第一种方式是显式地调用构造函数,第二种是隐式地调用构造函数。但注意第二种写法,不是第三行的这种形式。
Stock food = Stock("liorld Cabbage", 250, 1.25); //Explicit
Stock food("liorld Cabbage", 250, 1.25); //Implicit
Stock food(); //Declares a function return Stock class
我们知道,构造函数时在定义对象的时候自动执行的,默认构造函数是在未提供显式初始值时,用来创建对象的构造函数,例如:
Stock Hello;//uses the default constructor
如果没有提供任何构造函数,则C++将自动提供默认构造函数。它是默认构造函数的隐式版本,不做任何工作。对于Stock类来说,默认构造函数可能如下:
Stock::Stock(){ }//with nothing
因此将创建Hello对象,但不初始化其成员,这和我们使用int x;
创建x,但没有提供值给它一样。
非常奇怪的是,当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。为类定义了构造函数后,就必须为它提供默认构造函数。如果提供了非默认构造函数,但没有提供默认构造函数,则对象声明将出错:
Stock(const char * co, int n, double pr); //constructor declaration
/* ... */
Stock world;//Error! not possible with current constructor
如果要创建对象,而不显式地初始化,则必须定义一个不接受任何参数的默认构造函数。定义默认构造函数的方式有两种。一种是给已有构造函数的所有参数提供默认值(缺省设置),另一种方式是通过函数重载来定义另一个没有参数的构造函数:
Stock(const char * co = "ERROR", int n = 0, double pr = 0.0); //Default Value
Stock(); //Overload function
由于只能有一个默认构造函数,因此不要同时采用这两种方式。实际上,通常应初始化所有的对象,以确保所有成员一开始就有已知的合理值。因此,用户定义的默认构造函数通常给所有成员提供隐式初始值。
Stock::Stock() // default constructor
{
company = "No_named";
shares=0;
share_val=0;
total_val=0;
}
在设计类时,通常应提供对所有类成员做隐式初始化的默认构造函数。使用上述任何一种方式(没有参数或所有参数都有默认值)创建了默认构造函数后,便可以声明对象变量,而不对它们进行显式初始化。当然,如果我们有构造函数,且有默认值,显示初始化则是调用非默认构造函数,即接受参数的构造函数。
即我们可以同时这样写:
Stock(const char * co, int n = 0, double pr = 0.0); //Not default Value
Stock(); //Default Value
析构函数
我们初始化类可以使用构造函数,如果构造函数使用new来分配内存,则需要在对象被清理的时候使用delete来释放这些内存。C++中同样提供了,在对象过期时,程序将自动调用一个特殊的成员函数——析构函数。
和构造函数一样,析构函数的名称也很特殊:在类名前加上~。因此,Stock类的析构函数为~Stock( )。另外,和构造函数一样,析构函数也可以没有返回值和声明类型。与构造函数不同的是,析构函数没有参数,因此Stock析构函数的原型必须是这样的:
~Stock(); //Declaration
由于Stock没有使用new初始化,它的析构函数不需要承担任何重要的工作,因此可以将它定义为不执行任何操作的函数:
Stock::~Stock(){ } //Definition
如果创建的是静态存储类对象,则其析构函数将在程序结束时自动被调用。如果创建的是自动存储类对象(就像前面的示例中那样),则其析构函数将在程序执行完代码块时(该对象是在其中定义的)自动被调用。如果对象是通过new创建的,则它将驻留在栈内存或自由存储区中,当使用delete来释放内存时,其析构函数将自动被调用。
如果是在用户页面上时,main函数中我们一般在return 0;之前和类使用代码之后多加上一个{}。如果没有这些大括号,代码块将为整个main(),因此仅当main()执行完毕后,才会调用析构函数。在窗口环境中,这意味着将在两个析构函数调用前关闭,导致不存在返回值。添加后代码块位置发生改变,即可显示返回消息。例如:
int main()
{
{
/* uses class */
} //calls the noisy destructor
return 0;
}
由于在类对象过期时析构函数将自动被调用,因此必须有一个析构函数。如果我们没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。
构造函数和析构函数在类中的使用
我们把构造函数和析构函数在原来的Stock类中体现,改进一下这个类。我们先看类的声明,在.h头文件中:
// stock10.h – Stock class declaration with constructors, destructor added
#ifndef STOCK1_H_
#define STOCK1_H_
#include
class Stock
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot() { total_val = shares * share_val; }
public:
Stock(); // default constructor
Stock(const std::string & co, long n = 0, double pr = 0.0);
~Stock(); // noisy destructor
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
};
#endif
当然,我们在初始化的时候可以使用构造函数,然而我们想修改初始化的值的时候,也可以使用构造函数创建一个新的、临时的对象,然后将其内容复制给对象来实现的。
在C++11中,可将列表初始化语法用于类。只要提供与某个构造函数的参数列表匹配的内容,并用大括号将它们括起(就像结构一样):
struct person{
std::string Name;
int age;
};
person stu = {"Jack",18}; //valid initialization
class Stock{/* define before */}
Stock hot_tip = {"Derivatives Plus Plus", 100, 45.0); //valid initialization
还有,对于成员函数使用const声明时,该怎么写呢,我们先看这个:
const Stock land = Stock("Kludgehorn Properties") ;
land.show();
第二行会报错,?因为show( )的代码无法确保调用对象不被修改——调用对象和const一样,不应被修改。show( )方法没有任何参数。而且,它所使用的对象是由方法调用隐式地提供的。C++提供的解决方法是将const关键字放在函数的括号后面。
void show() const; //Declaration promises not to change invoking object
void stock::show() const //Definition promises not to change invoking object
以这种方式声明和定义的类函数被称为const成员函数。就像应尽可能将const引用和指针用作函数形参一样,只要类方法不修改调用对象,就应将其声明为const。
小结
构造函数是一种特殊的类成员函数,在创建类对象时被调用。构造函数的名称和类名相同,但通过函数重载,可以创建多个同名的构造函数,条件是每个函数的特征标(参数列表)都不同。另外,构造函数没有声明类型。通常,构造函数用于初始化类对象的成员,初始化应与构造函数的参数列表匹配。
就像对象被创建时程序将调用构造函数一样,当对象被删除时,程序将调用析构函数。每个类都只能有一个析构函数。析构函数没有返回类型(连void都没有),也没有参数,其名称为类名称前加上~。如果构造函数使用了new,则必须提供使用delete的析构函数。
This指针
我们在Stock类声明成员函数可以显示数据,但它缺乏分析能力,即如果多个对象时,成员函数如何处理数据。这个时候C++引入了this指针来解决这样的问题。
this指针是一个指针,里面放置的是当前对象的地址(成员函数执行时,调用该成员函数的对象),this指针是类“成员函数”的一个隐藏的参数,该指针存着调用成员函数的对象的地址(当前对象)这样就可以分辨成员自身,下面是一个例子:
这是头文件的声明内容:
// stock20.h -- augmented version
#ifndef STOCK20_H_
#define STOCK20_H_
#include
class Stock
{
private:
std::string company;
int shares;
double share_val;
double total_val;
void set_tot() { total_val = shares * share_val; }
public:
// Stock(); // default constructor
Stock(const std::string & co, long n = 0, double pr = 0.0);
~Stock(); // do-nothing destructor
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show()const;
const Stock & topval(const Stock & s) const;
};
#endif
这是一些定义(不全):
Stock::Stock() // default constructor
{
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
Stock::Stock(const std::string & co, long n, double pr)
{
company = co;
if (n < 0)
{
std::cout << "Number of shares can't be negative; "
<< company << " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
// class destructor
Stock::~Stock() // quiet class destructor
{
}
//This pointer
const Stock & Stock::topval(const Stock & s) const
{
if (s.total_val > total_val)
return s;
else
return *this;
}
例如我们定义两个对象Box1,Box2,我们来看一下以下语句的返回情况:
Stock Box1(3.3, 1.2, 1.5); // Declare box1
Stock Box2(8.5, 6.0, 2.0); // Declare box2
Stock *top;
top = Box1.topval(Box2); //same as top = Box2.topval(Box1);
Top表示两个对象中total_val最大的,我们看函数的定义,if语句那里total_val前其实有一个隐式的指针,即this指针,表示Box1,而Box2作为参数s传入进行比较。最后返回若返回隐式的名称,则是对this指针进行访问即可。
关于this指针,我们还需注意以下几点:
- 只能在成员函数中使用。
- this指针的类型:类 类型* const
- this指针没有存储在对象中,因此不会影响对象的大小,而是在成员函数运行时,时时刻刻指向当前对象。
- this指针是“成员函数”第一个隐藏的参数,“隐藏的”—用户在实现成员函数时,不用显式给出,该参数是编译器自动添加的也是编译器自动来进行传递的。
最后用CSDN一个博主的精辟的话来总结:
当你进入一个房子后,
你可以看见桌子、椅子、地板等,
但是房子你是看不到全貌了。
对于一个类的实例来说,
你可以看到它的成员函数、成员变量,
但是实例本身呢?
this是一个指针,
它时时刻刻指向你这个实例本身。
对象数组
上面的情况是在两个对象的时候处理的,但我们如果有很多个对象的时候呢?这时候就需要像其他多个数据一样,使用对象数组。声明对象数组的方法与声明标准类型数组相同:
Stock mystuff[4]; // creates an array of 4 Stock objects
前面提到,当程序创建未被显式初始化的类对象时,总是调用默认构造函数。上述声明要求,这个类要么没有显式地定义任何构造函数(在这种情况下,将使用不执行任何操作的隐式默认构造函数),要么定义了一个显式默认构造函数(有缺省值)。
当我们使用对象数组的时候,把它每一个元素都当成一个对象使用,都可以执行其成员函数。
类作用域
我们以前在C中介绍了全局(文件)作用域和局部(代码块)作用域。可以在全局变量所属文件的任何地方使用它,而局部变量只能在其所属的代码块中使用。函数名称的作用域也可以是全局的,但不能是局部的。C++类引入了一种新的作用域:类作用域。
在类中定义的名称(如类数据成员名和类成员函数名)的作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。因此,可以在不同类中使用相同的类成员名而不会引起冲突。例如,Stock类的shares成员不同于Stock2类的shares成员。另外,类作用域意味着不能从外部直接访问类的成员,公有成员函数也是如此。也就是说,要调用公有成员函数,必须通过对象的访问。
所以,在定义成员函数时,必须使用作用域解析运算符(class::)。
在类声明或成员函数定义中,可以使用未修饰的成员名称(未限定的名称),就像sell( )调用set_tot( )成员函数时那样。构造函数名称在被调用时,才能被识别,因为它的名称与类名相同。在其他情况下,使用类成员名时,必须根据上下文使用直接成员运算符(普通对象用)(.)、间接成员运算符(指针对象用)(->)或作用域解析运算符(类函数用)(::)。
作用域为类的常量
我们在类中可能会定义一个常量,我们可能会想这么写:
const int Months= 12;
但是这样的写法是错误的,因为声明类只是描述了对象的形式,并没有创建对象。因此,在创建对象前,将没有用于存储值的空间(C++11提供了成员初始化,但不适用于前述数组声明)。我们有两种方法来代替这样的写法:
第一种方式是在类中声明一个枚举。在类声明中声明的枚举的作用域为整个类,因此可以用枚举为整型常量提供作用域为整个类的符号名称。
enum {Months = 12};
注意,用这种方式声明枚举并不会创建类数据成员。也就是说,所有对象中都不包含枚举。由于这里使用枚举只是为了创建符号常量,并不打算创建枚举类型的变量,因此不需要提供枚举名。
C++提供了另一种在类中定义常量的方式——使用关键字static:
static const int Months= 12;
这将创建一个名为Months的常量,该常量将与其他静态变量存储在一起,而不是存储在对象中。因此,不同的对象将共享这一个常量。在C++98中,只能使用这种技术声明值为整数或枚举的静态常量,而不能存储double常量。C++11消除了这种限制。
总结
面向对象编程强调的是程序如何表示数据。使用OOP方法解决编程问题的第一步是根据它与程序之间的接口来描述数据,从而指定如何使用数据。然后,设计一个类来实现该接口。一般来说,私有数据成员存储信息,公有成员函数(又称为方法)提供访问数据的唯一途径。类将数据和方法组合成一个单元,其私有性实现数据隐藏。
通常,将类声明分成两部分组成,这两部分通常保存在不同的文件中。类声明(包括由函数原型表示的方法)应放到头文件中。定义成员函数的源代码放在方法文件中。这样便将接口描述与实现细节分开了。从理论上说,只需知道公有接口就可以使用类。
类是用户定义的类型,对象是类的实例。这意味着对象是这种类型的变量,例如由new按类描述分配的内存。C++让用户定义的类型尽可能与标准类型类似,因此可以声明对象、指向对象的指针和对象数组。可以按值传递对象、将对象作为函数返回值、将一个对象赋给同类型的另一个对象。如果提供了构造函数,则在创建对象时,可以初始化对象。如果提供了析构函数方法,则在对象消亡后,程序将执行该函数。
后记
好了,至此C++类专题中第一节,对象和类就结束了,这个专题的文字量估计很大,也是C++学习的重点,所以细节要到位。下一章 使用类 见~
Reference:《C++ Primer Plus 6th》第10章
Comments NOTHING