[C++学习笔记] 第 7 章 对象和类

第 7 章 对象和类

7.1 简介类

类是一种将抽象转换为用户定义类型的 C++ 工具,它将数据表示和操纵数据的方法组合成一个整体。一般来说,类规范由两个部分组成:

  • 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口。
  • 类方法定义:描述如何实现类成员函数。

​ 通常,C++ 程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。为帮助识别类,通常遵循一种常见但不通用的约定——将类名首字母大写。下面是一个示例:

stock00.h

#ifndef STOCK00_H_
#define STOCK00_H_
#include <string>

class Stock
{
private:
	std::string company;
	long shares;
	double share_val;
	double total_val;
	void set_total() { 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();
};

#endif

​ 不同于模板参数,在类中 classtypename 不是同义词,不能使用 typename 代替 class

  1. 访问控制

    ​ 关键字 privatepublic 是新的,它们描述了对类成员的访问控制。使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数(或友元函数)来访问对象的私有成员。因此,公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。防止程序直接访问数据被称为数据隐藏。

    ​ 类设计应尽可能将公有接口(成员函数)与其具体实现分开。公有接口表示设计的抽象组件,将实现细节放在一起并将它们与抽象分开被称为封装。

  2. 控制对成员的访问:公有还是私有

    ​ 无论类成员是数据成员还是成员函数,都可以在类的公有部分或私有部分中声明它。但由于隐藏数据是 OOP 的主要目标之一,因此数据项通常放在私有部分,成员函数放在公有部分;否则,就无法从程序中调用这些函数。也可以把成员函数放在私有部分中,虽然不能直接从程序中调用这种函数,但公有办法却可以使用它们。

    ​ 不必在类声明中使用 private,因为这是对类对象的默认访问控制。但是为了强调数据隐藏的概念,也可以显示地使用 private

    类和结构

    C++ 对结构进行了扩展,使之具有与类相同的特性。它们之间唯一的区别是,结构的默认访问类型是 public,而类为 private。C++ 程序员通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象。

7.1.1 实现类成员函数

​ 还需要创建类描述的第二部分:为那些由类声明中的原型表示的成员函数提供代码。成员函数定义与常规函数定义非常相似,但有两个特殊的特征:

  • 定义成员函数时,使用作用域解析运算符 :: 来标识函数所属的类;
  • 类方法可以访问 private 组件。

​ 下面是 Stock 类成员函数的实现:

stock00.cpp

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include "stock00.h";

void Stock::acquire(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();
}

void Stock::buy(long num, double price)
{
	if (num < 0)
		std::cout << "Number of shares purchased can't be negative. " << "Transaction is aborted.\n";
	else
	{
		shares += num;
		share_val = price;
		set_tot();
	}
}

void Stock::sell(long num, double price)
{
	using std::cout;
	if (num < 0)
		cout<< "Number of shares sold can't be negative. " << "Transaction is aborted.\n";
	else if(num > shares)
		cout<<"You can't sell more than you have! "<< "Transaction is aborted.\n";
	else
	{
		shares -= num;
		share_val = price;
		set_tot();
	}
}

void Stock::update(double price)
{
	share_val = price;
	set_tot();
}

void Stock::show()
{
	std::cout << "Company: " << company
		<< "Shares: " << shares << '\n'
		<< "Share Price:  $" << share_val
		<< "Total Worth: $" << total_val << '\n';
}
  1. 成员函数说明

    4 4 4 个成员函数设置或重新设置了 total_val 成员值。这个类并非将计算代码编写 4 4 4 次,而是让每个成员函数都调用 set_tot() 函数。由于 set_tot() 只是实现代码的一种方式,而不是公有接口的组成部分,因此这个类将其声明为私有成员函数(即,编写类的人可以使用它,而使用类的人不能使用)。

  2. 内联方法

    其定义位于类声明中的函数都将自动成为内联函数,因此 Stock::set_tot() 是一个内联函数。类声明常将短小的成员函数作为内联函数,set_tot() 符合这样的要求。

    如果愿意,也可以在类声明之外定义成员函数,并使其成为内联函数,只需在类实现部分中定义函数时使用 incline 即可:

    class Stock
    {
    private:
        ...
        void set_tot();
    public:
        ...
    };
    inline void Stock::set_tot //放在头文件中
    { total_val = shares * share_val; }
    

    内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义,最简单的办法就是将内联函数的定义放在定义类的头文件中

  3. 使用类

    下面是一个使用该类的程序:

    #include <iostream>
    #include "stock00.h"
    int main()
    {
    	Stock a;
    	a.acquire("NanoSmart", 20, 12.50);
    	a.show();
    	a.buy(15, 18.125);
    	a.show();
    	a.sell(400, 20.00);
    	a.buy(300000, 40.125);
    	a.show();
    	a.sell(300000, 0.125);
    	a.show();
    	return 0;
    }
    

7.2 类的构造函数和析构函数

​ C++ 的目标之一是让使用对象就像使用标准类型一样。然而,到目前为止并不能使用常规的初始化语法初始化对象,如:

Stock hot = {"Sukie's Autos, Inc.", 200, 50.25}; //编译错误!

​ 不能像上面这样初始化 Stock 对象的原因在于,数据部分的访问状态是私有的,这意味着程序不能直接访问数据成员。为此,C++ 提供了一个特殊的成员函数——类构造函数,专门用于构造新对象,将值赋给它们的数据成员。构造函数的名称与类名相同,且没有返回类型。

7.2.1 声明和定义构造函数

​ 仍以 Stock 类为例。首先,程序员可能只想设置 company 成员,而将其他值设置为 0 0 0,这可以通过默认参数来实现,因此,构造函数的原型如下:

Stock(const string & co, long n = 0, double pr = 0.0);

​ 注意,构造函数没有返回类型,且原型应该放在 public 部分。下面是其定义的示例:

Stock::Stock(const std::string& co, long n = 0, double pr = 0.0)
{
	company = co;
	if (n < 0)
	{
		std::cerr << "Number of shares can't be negative; " << company << " shares set to 0.\n";
		shares = 0;
	}
	else
		shares = n;
	share_val = pr;
	set_tot();
}
7.2.2 使用构造函数

​ C++ 提供了两种使用构造函数来初始化对象的方式。第一种方法是显式地调用构造函数:

Stock food = Stock("World Cabbage", 250, 1.25);

​ 另一种是隐式地调用构造函数:

Stock garment("Furry Mason", 50, 2.5);

​ 每次创建类对象时,C++ 都会使用构造函数。下面是将构造函数与 new 一起使用的方法:

Stock *pstock = new Stock("Electroshock Games", 18, 19.0);
7.2.3 默认构造函数

​ 默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。例如,下面的语句将调用默认构造函数:

Stock baymax;

​ 如果没有提供任何构造函数,则 C++ 将自动提供默认构造函数,此函数将不会完成任何工作,就像 int x; 一样将创建变量,但没有提供任何值(变量内部存垃圾值)。

​ 注意,当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。为类定义了构造函数后,程序员就必须为它提供默认构造函数,否则语句 Stock baymax; 将报错。

​ 这样做的原因是为了禁止创建未初始化的对象。如果想要创建对象,而不显式地初始化,则必须定义一个不接受任何参数的默认构造函数。定义默认构造函数的方式有两种:

  1. 一种是给已有构造函数的所有参数提供默认值:

    Stock(const string & co = "Error", long n = 0, double pr = 0.0);
    
  2. 另一种是通过函数重载来定义另一个构造函数——一个没有参数的构造函数,如 Stock();

    Stock::Stock()
    {
        company = "no name";
        shares = 0;
        share_val = 0.0;
        total_val = 0.0;
    }
    

​ 由于只能有一个默认构造函数,因此不要同时采用以上两种方式。

​ 创建完默认构造函数后,便可以按以下形式调用:

Stock first;				//隐式调用默认构造函数
Stock second = Stock();		//显式调用默认构造函数
Stock *third = new Stock;	//隐式调用默认构造函数
Stock fourth(); 			//错误!这是声明一个函数

​ 注意最后一种调用方式是错误的!此时 fourth 是一个返回 Stock 对象的函数。

7.2.4 析构函数

​ 用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数——析构函数,该函数将完成对象的清理工作。例如,如果构造函数使用了 new 来分配内存,则析构函数将使用 delete 来释放这些内存。

Stock 的构造函数没有使用 new,因此析构函数实际上没有需要完成的任务。在这种情况下,只需要让编译器生成一个什么都不做的隐式析构函数即可。

​ 和构造函数一样,析构函数的名称也很特殊:在类名前加上 ~,且没有任何参数。因此,Stock 类的析构函数为 ~Stock()

​ 什么时候该调用析构函数呢?这由编译器决定,通常不应在代码中显示地调用析构函数。如果创建的是静态存储类对象,则其析构函数将在程序结束时自动调用。如果创建的是自动存储类对象,则其析构函数将在程序执行完代码块时自动调用。如果对象是通过 new 创建的,则当使用 delete 时其析构函数将自动调用。

​ 由于在类对象过期时析构函数将自动被调用,因此必须有一个析构函数。如果程序员没有提供析构函数,则编译器将隐式地声明一个默认析构函数。

7.2.5 改进 Stock

​ 给 Stock 加上构造函数和析构函数后,其具体实现如下:

stock10.h

#pragma once
#ifndef STOCK10_H_
#define STOCK10_H_

#include <string>

class Stock
{
private:
	std::string company;
	long shares;
	double share_val;
	double total_val;
	void set_tot() { total_val = shares * share_val; }
public:
	Stock(const std::string& co, long n = 0, double pr = 0.0);
	Stock();
	~Stock();
	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();
};

#endif

stock10.cpp

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include "stock10.h"

Stock::Stock(const std::string& co, long n , double pr)
{
	company = co;
	if (n < 0)
	{
		std::cerr << "Number of shares can't be negative; " << company << " shares set to 0.\n";
		shares = 0;
	}
	else
		shares = n;
	share_val = pr;
	set_tot();
}

Stock::Stock()
{
	company = "no name";
	shares = 0;
	share_val = 0.0;
	total_val = 0.0;
}

void Stock::acquire(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();
}

void Stock::buy(long num, double price)
{
	if (num < 0)
		std::cout << "Number of shares purchased can't be negative. " << "Transaction is aborted.\n";
	else
	{
		shares += num;
		share_val = price;
		set_tot();
	}
}

Stock::~Stock()
{
	std::cout << "Bye, " << company << "!\n";
}

void Stock::sell(long num, double price)
{
	using std::cout;
	if (num < 0)
		cout<< "Number of shares sold can't be negative. " << "Transaction is aborted.\n";
	else if(num > shares)
		cout<<"You can't sell more than you have! "<< "Transaction is aborted.\n";
	else
	{
		shares -= num;
		share_val = price;
		set_tot();
	}
}

void Stock::update(double price)
{
	share_val = price;
	set_tot();
}

void Stock::show()
{
	std::cout << "Company: " << company
		<< " Shares: " << shares << '\n'
		<< " Share Price:  $" << share_val
		<< " Total Worth: $" << total_val << '\n';
}

usestok1.cpp

#include <iostream>
#include "stock10.h"
int main()
{
	using std::cout;
	cout << "Using constructors to create new objects\n";
	Stock stock1("NanoSmart", 12, 20.0);
	stock1.show();
	Stock stock2 = Stock("Boffo Object", 2, 2.0);
	stock2.show();

	cout << "Assigning stock1 to stock2:\n";
	stock2 = stock1;
	cout << "Listing stock1 and stock2:\n";
	stock1.show();
	stock2.show();
	
	cout << "Using a constructor to reset an object\n";
	stock1 = Stock("Nifty Foods", 10, 50.0);
	cout << "Revised stock1:\n";
	stock1.show();
	cout << "Done.\n";

	return 0;
}
//输出如下:
Using constructors to create new objects
Company: NanoSmart Shares: 12
 Share Price: $20.000 Total Worth: $240.00
Company: Boffo Object Shares: 2
 Share Price: $2.000 Total Worth: $4.00
Assigning stock1 to stock2:
Listing stock1 and stock2:
Company: NanoSmart Shares: 12
 Share Price: $20.000 Total Worth: $240.00
Company: NanoSmart Shares: 12
 Share Price: $20.000 Total Worth: $240.00
Using a constructor to reset an object
Bye, Nifty Foods!
Revised stock1:
Company: Nifty Foods Shares: 10
 Share Price: $50.000 Total Worth: $500.00
Done.
Bye, NanoSmart!
Bye, Nifty Foods!
  1. 程序说明

​ 上述程序使用了两种初始化对象的方式,第一种是 Stock stock1("NanoSmart", 12, 20.0);,第二种是 Stock stock2 = Stock("Boffo Object", 2, 2.0);。C++ 标准允许编译器使用两种方式来执行第二种语法。一种是使其行为与第一种初始化方式完全一致,而另一种是允许调用构造函数来创建一个临时对象,然后将该临时对象复制到 stock2 中,并丢弃它。如果编译器使用的是这种方式,则将为临时对象调用析构函数。

​ 构造函数不仅仅可以用于初始化新对象,还可以用来赋值。例如,程序中语句 stock1 = Stock("Nifty Foods", 10, 50.0);stock1 对象已经存在,因此这条语句不是对 stock1 进行初始化,而是将新值赋给它。这是通过让构造函数创建一个新的临时对象,然后将其内容赋值给 stock1 来实现的。随后程序将调用析构函数,以删除临时对象。

  1. C++11 列表初始化

    在 C++11 中,可将列表初始化语法用于类,只要提供与某个构造函数的参数列表匹配的内容,如:

    Stock a = {"abc", 100, 45.0};	//匹配构造函数
    Stock b {"abc"};				//匹配含有默认参数的构造函数
    Stock c {};						//匹配默认构造函数
    
  2. const 成员函数

    const Stock land = Stock("abc");
    land.show();
    

    ​ 上述代码中,编译器将拒绝第二行,因为 show() 的代码无法保证 land 的数据成不会被修改。为确保成员函数不会修改数据成员,需要将 const 放在函数括号的后面。例如,函数声明为 void show() const;,函数定义开头为 void stock::show() const

    ​ 以这种方式声明和定义的类成员函数被称为 const 成员函数。就像应尽可能将 const 引用和指针用作函数形参一样,只要类方法不修改调用对象,就应将其声明为 const

7.2.6 构造函数和析构函数小结

​ 构造函数是一种特殊的类成员函数,在创建类对象时被调用。构造函数的名称和类名相同,但通过函数重载,可以创建多个同名的构造函数,条件是每个函数的特征标(参数列表)都不同。另外,构造函数没有声明类型。

​ 此外,接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值:

Classname object = value;

​ 这种特性可能导致问题,但可以被关闭(将在以后介绍)。

​ 默认构造函数没有参数,因此如果创建对象时没有显式地初始化,则将调用默认构造函数。如果程序中没有提供任何构造函数,则编译器会为程序定义一个默认构造函数。否则,必须自己提供默认构造函数。

​ 当对象被删除时,程序将调用析构函数。每个类都只能有一个析构函数。析构函数没有返回类型(连 void 也没有),也没有参数,其名称为类名称前加上 ~。如果构造函数使用了 new,则必须提供使用 delete 的析构函数。

7.3 this 指针

​ 到目前为止,Stock 类的成员函数只涉及一个对象。但有时候成员函数可能需要涉及到两个对象,在这种情况下需要使用 C++ 指针。例如,需要编写一个方法,将对象与另一个对象进行比较并返回其中的较大值,如:

const Stock& Stock::topval(const Stock& s) const
{
	if (s.total_val> total_val)
		return s;
	else
		return ???;
}

​ 上述代码中,如果 s.total_val 大于 total_val,则函数将返回指向 s 的引用;否则,将返回用来调用该方法的对象。问题在于应该如何称呼这个对象?

​ C++ 解决这种问题的方法是:使用被称为 this 的特殊指针。this 指针指向用来调用成员函数的对象。因此代码中 ??? 部分可以写成 *this

注意:

每个成员函数(包括构造函数和析构函数)都有一个 this 指针。this 指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式 *this。在函数的括号后面使用 const 限定符会将 this 限定为 const,这样将不能使用 this 来修改对象的值。

7.4 对象数组

​ 声明对象数组的方法与声明标准类型数组相同,如 :

Stock mystuff[4];
Stock stocks[4] = {
    Stock("NanoSmart", 12.5, 20),
    Stock("Boffo Objects", 200, 2.0),
    Stock(),//显式地调用默认构造函数
    Stock("Fleep Enterprises", 60, 6.5)
};

​ 上述声明有一个要求:类必须要有默认构造函数。因为初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。因此,要创建类对象数组,则这个类必须有默认构造函数。

​ 下面是使用对象数组的一个示例:

#include <iostream>
#include "stock10.h"

const int STKS = 4;
int main()
{
    Stock stocks[4] = {
    Stock("NanoSmart", 12.5, 20),
    Stock("Boffo Objects", 200, 2.0),
    Stock("Monolithic Obelisks", 130, 3.25),
    Stock("Fleep Enterprises", 60, 6.5)
    };

    std::cout << "Stock holdings:\n";
    int st;
    for (st = 0; st < STKS; st++)
        stocks[st].show();
    const Stock* top = &stocks[0];
    for (st = 1; st < STKS; st++)
        top = &top->topval(stocks[st]);
    std::cout << "\nMost valuable holding:\n";
    top->show();
    return 0;
}

7.5 类作用域

​ C++ 类引入了一种新的作用域:类作用域。在类中定义的名称的作用域为整个类,只在该类中可知,在类外是不可知的。因此,可以在不同类中使用相同的类成员名而不会引起冲突。另外,类作用域意味着不能从外部直接访问类的成员,公有函数也是如此,也就是说要调用公有成员函数,必须通过对象。

7.5.1 作用域为类的常量

​ 有时候,使符号常量的作用域为类很有用。例如,类声明可能使用字面值 30 30 30 来指定数组的长度,由于该常量对于所有的对象来说都是相同的,因此创建一个由所有对象共享的常量是个很不错注意。有人可能认为下面这样可行:

class Bakery
{
private:
    const int Months = 12;
    double costs[Months];
    ...
}

​ 但这是行不通的,因为声明类只是描述了对象的形式,并没有创建对象。因此,在创建对象前,将没有用于存储值的空间(但 C++11 提供了成员初始化,使得语句 const int Months = 12; 可以通过编译,而 double costs[Months]; 仍会报错)。

​ 有两种方法可以解决这个问题,并且效果相同:

  • 第一种方式是在类中声明一个枚举。

    在类声明中声明的枚举的作用域为整个类,因此可以用枚举为整型常量提供作用域为整个类的符号名称。如:

    class Bakery
    {
    private:
        enum { Months = 12 };
        double costs[Months];
        ...
    }
    

    注意,用这种方式声明枚举并不会创建类数据成员。也就是说,所有的对象都不会包含枚举。另外,Months 只是一个符号名称,在作用域为整个类的代码中遇到它时,编译器将用 12 12 12 来替换它。由于这里使用枚举只是为了创建符号常量,并不打算创建枚举类型的变量,因此不需要提供枚举名。

  • 第二种方式是使用关键字 static:

    class Bakery
    {
    private:
        static const int Months = 12;
        double costs[Months];
        ...
    }
    

    这将创建一个名为 Months 的常量,该常量将与其他静态变量存储在一起,而不是存储在对象中。因此,只有一个 Months 常量,为所有 Bakery 对象所共享。

7.5.2 作用域内枚举

​ 传统的枚举存在一些问题,其中之一是两个枚举定义中的枚举量可能会发生冲突。例如:

enum egg { Small, Medium, Large };
enum t_shirt { Small, Medium, Large };

​ 这将无法通过编译,因为它们位于相同的作用域内,将发生冲突。为避免这种问题,C++11 提供了一种新枚举,其枚举量的作用域为类。这种枚举的声明类似于下面这样:

enum class egg { Small, Medium, Large };
enum class t_shirt { Small, Medium, Large };

​ 可以使用关键字 struct 代替 class,枚举量的作用域为类后,不同枚举定义中的枚举量就不会发生名称冲突了,但需要使用作用域解析运算符,如:

egg choice = egg::large;
t_shirt Floyd = t_shirt::Large;

​ C++11 还提高了作用域内枚举的类型安全。在有些情况下,常规枚举将自动转换为整型,但作用域内枚举不能隐式地转换为整型(必要时,可以进行显式转换),如:

enum egg_old { Small, Medium, Large };
enum class t_shirt { Small, Medium, Large };
int a = Small;
int b = t_shirt::Small;	//将报错,作用域枚举不能隐式地转换为int
int c = int(t_shirt::Small);

​ C++11 中,对于作用域内枚举,其底层类型为 int(C++98 中枚举的底层类型如何选择取决于实现)。另外,还提供了 一种语法,可用于做出不同的选择:

enum class : short t_shirt { Small, Medium, Large };

:short 将底层指定为 short。注意,底层类型必须为整型。在 C++11 中,也可使用这种语法来指定常规枚举的底层类型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值