C++ Primer第五版之第7章 函数

类的基本思想:数据抽象和封装

  • 数据抽象:一种依赖于接口和实现分离的编程技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数
  • 封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节
    • 定义在类内部的函数是隐式的inline函数(内联函数)
    • const成员函数(const紧跟在参数列表后面):表示this是一个常量指针
    • 如果非成员函数是类接口的一部分,则这些函数的声明应该与类在同一个头文件中

定义抽象数据类型

构造函数

  • 某些类不能依赖于合成的默认构造函数。
  • 只有当类没有声明任何构造函数,编译器才会自动生成默认构造函数。
  • 如果类包含有内置类型或者复合类型的成员时,则只有当这些成员全部被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。
  • =default:显式声明默认构造函数

  • const对象在完成构造函数对其初始化之后才真正获取其“常量”属性
  • 默认构造函数:提供没有初始值的类成员默认初始化
  • 封装是指保护类的成员不被随意访问的能力。通过把类的实现细节设置为private,我们就能完成类的封装,从而实现类的接口和实现的分离。
  • 封装的好处:1.确保用户代码不会破坏封装对象的状态;2.被封装的类的具体细节可以随时改变,而无需调整用户级别的代码
  • 构造函数的初始值有时必不可少。有时我们可以忽略数据成员初始化和赋值之间的差异,但并不总是这样子。当成员是const或者是引用时就必须初始化。
  • 因此建议使用构造函数初始值进行初始化,一方面可以提高效率,一方面可以避免某些意想不到的编译错误
  • 类成员初始化顺序和他们在类中出现的顺序一致,尽量让构造函数的初始值顺序同其成员声明顺序保持一致

访问控制与封装

  • 在C++中,struct与class类似,既可以包含成员变量,又可以包含成员函数
  • C++中的struct与class基本是通用的,只有几个细节不同:
    • 使用class时,类中的成员默认都是private属性的;而使用struct时,结构体中的成员默认都是public属性的
    • class继承默认是private继承,而struct继承默认是public继承
    • class可以使用模板,而struct不能
  • 当你觉得你要做的更像是一种数据结构时,那么用struct,如果你要做的更像是一种对象时,那么用class
  • 然而对于访问控制,应该在程序中明确的指出,而不是依靠默认
  • 友元
    • 类可以允许其他类或其他函数访问它的非公有成员,则令其他类或其他函数成为它的友元
    • 一般来说,最好在类定义开始或结束前的位置集中声明友元
class Sales_data{
friend Sales_data add(const Sales_data&, const Sales_data&);
/*......*/
public:
    /*.....*/
private:
    /*.....*/
};

类的其他特性

  • 可变数据成员:在变量的声明中加入mutable关键字,即使在一个const对象内也能修改。
class Screen{ 
public:
    void some_number() const; 
private:
    mutable size_t access_ctr;
};

void Screen::some_number() const
{
    ++access_ctr;
}
  • 返回*this的成员函数:对于此类成员函数,如果定义的返回类型不是引用,则成员函数的返回值将是*this的副本,因此调用此类成员函数只能改变其副本,而不能更改其对象的值
  • 一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用
  • 对于一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明
  • 友元关系不存在传递性;每个类负责控制自己的友元类或友元函数;令某个类的成员函数作为自己的友元,必须声明该成员函数属于哪个类

类的作用域

  • 一旦遇到类名,定义的剩余部分就在类的作用域之内了,结果就是我们可以直接使用类的成员而无须再次授权了。
  • 编译器处理完类中的全部声明后才会处理成员函数的定义。也就是直到类全部可见后才编译函数体。

构造函数再探

隐式类型转换

  • 如果成员是const、引用或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始值列表为这些成员提供初始值
  • 如果构造函数只接受一个实参,那么它实际上定义了转换为此类类型的隐式转换机制,这种构造函数称为转换构造函数
  • 编译器只能自动执行一步类型转换
  • 可以通过在类内将构造函数声明为explicit阻止隐式类型转换,explicit只对一个参数的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换
  • explicit只能出现在类内构造函数声明处,且explicit构造函数只能用于直接初始化。
  • 可以为转换显式地使用构造函数,如static_cast。
string null_book = "9-999-99999-9";
item.combine(null_book); //string类型隐式转换为Sales_data类型

// 只允许一步类类型转换
item.combine("9-999-99999-9"); //错误:需要两步类型转换
item.combine(string("9-999-99999-9"));
item.combine(Sales_data("9-999-99999-9"));

explicit Sales_data::Sales_data(istream &is)   //错误
{
    read(is, *this);
}
//explicit构造函数只能用于直接初始化
Sales_data item(null_book); //直接初始化
Sales_data item = null_book; //错误:不能用于拷贝形式的初始化

委托构造函数

  • 使用它所属类的其他构造函数执行它自己的初始化过程

类的静态成员

  • 类的静态成员用static标识,使得其与类关联在一起
  • 类的静态成员和静态成员函数存在于任何对象之外,对象中不包含任何与静态成员数据有关的数据
  • 静态函数不包含this指针,且不能被声明为const
  • 通过作用域运算符直接访问静态成员,成员函数不用通过作用域运算符就能直接使用静态成员。
  • 可以用类的对象、引用或者指针来访问静态成员
  • static必须出现在类中,但static函数可以在类外定义,不能重复static关键字
  • 因为静态数据成员不属于类的任何一个对象,也就是说他们不是由类的构造函数初始化的。
  • 必须在类的外部定义和初始化每个静态成员。一个静态数据成员只能定义一次。而静态成员函数既可以在类的内部定义也可以在类的外部定义。
  • 静态成员的类内初始化:类内可以初始化const或者constexpr的成员
  • 静态成员可以作为默认实参
static constexpr int period = 30; //在类内提供初始值
constexpr int Account::period; // 即使一个常量静态数据成员在类内部初始化了,也应该在类的外部定义一下该成员。

部分实现代码: 

#include<string>
using std::string;
#include<iostream>
using namespace std;
class Screen {
public:
	//类还可以自定义某种类型在类中的别名,可以隐藏Screen实现的细节
	typedef string::size_type pos; 
	//using pos = string::size_type;

	Screen() = default;
	Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c)
	{ }
	// 读取光标处字符
	// 类内成员函数为隐式内联
	char get() const
	{
		return contents[cursor];
	}
	
	//显式内联
	inline char get(pos ht, pos wd) const;

	Screen &move(pos r, pos c);

	void some_member() const;

	// 返回值为引用说明返回为对象本身而不是对象的拷贝,这样子理解返回值是否为引用好像比较舒服一点
	Screen &set(char);
	Screen &set(pos, pos, char);
	Screen &display(ostream &os)
	{
		do_display(os);
		return *this;
	}

	//基于const的重载
	const Screen &display(ostream &os) const
	{
		do_display(os);
		return *this;

	}

private:
	pos cursor = 0;
	pos height = 0, width = 0;
	string contents;
	// mutable关键字标识可变数据成员,即使const成员函数也可以修改
	// mutable size_t access_ctr = 0;
	//建议对公共代码使用私有功能函数,一般用于const和非const重载版本的一个功能函数
	void do_display(ostream &os) const
	{
		os << contents;
	}

};

// 类外定义为内联
inline Screen &Screen::move(pos r, pos c)
{
	cursor = r * width + c;
	return *this;
}

char Screen::get(pos ht, pos wd) const
{
	return contents[ht * width + wd];
}

//void Screen::some_member() const
//{
//	//即使const成员函数也可以修改access_ctr的值
//	++access_ctr;
//}

Screen &Screen::set(char c)
{
	contents[cursor] = c;
	return *this;
}

Screen &Screen::set(pos row, pos col, char c)
{
	contents[row * width + col] = c;
	return *this;
}

#endif
//Sales_data.h
//
// Created by like0 on 2019/2/28.
//

#ifndef CHAPTER7_SALES_DATA_H
#define CHAPTER7_SALES_DATA_H

#include <iostream>
using namespace std;
#include<string>
using std::string;


class Sales_data
{
	//所有的成员函数必须在类内部声明,在类外部定义,定义在类内部的函数是隐式的inline函数;定义在外部默认不是内联的
	// 友元声明不受访问说明符的约束,一般在类开头或者结尾声明
	// 一个类也可作为友元,一个类的成员函数也可以成为另一个类的友元(需要通过作用域标识符说明)
	friend Sales_data add(const Sales_data&, const Sales_data&);
	friend ostream& print(ostream&, const Sales_data&);
	friend istream& read(istream&, Sales_data&);
	//C++默认成员属性为private,外部函数无法防伪
	//我擦,这个bug找了很久,加了个public就好了
	//访问说明符public成员定义类的接口,private部分则封装了类的实现细节
public:
	//构造函数
	//类可以不提供构造函数,编译器会自动提供默认构造函数
	//默认构造函数可以没有参数,也可以通过提供默认参数的形式
	//默认构造函数,当定义了其他构造函数,最好也提供一个默认构造函数
	//内置类型数据被提供了初始值时默认构造函数才有效

	// 类内定义的构造函数
	// 冒号和花括号之间的部分:构造函数初始值列表
	// 非委托构造函数
	Sales_data(const string &s, unsigned n, double p) :
		bookNo(s), units_sold(n), revenue(p*n) {
		cout << "all" <<endl;
	}

	// 默认构造函数,委托三参构造函数
	Sales_data() : Sales_data("", 0, 0) {
		cout << "default" << endl;
	}

	Sales_data(const string &s) : Sales_data(s, 0, 0) {
		cout << "string" << endl;
	}

	//委托默认构造函数
	Sales_data(istream &is) : Sales_data()
	{
		read(is, *this);
		cout << "istream&" << endl;
	}


	


	//Sales_data(const string &s) :
	//	bookNo(s) { }


    // 对Sale_data类的操作
    // 所有成员必须在类内声明,但可以在类外定义
    // 参数列表之后的const的作用是修改隐式this指针的类型
    // 默认情况下this为指向非常量对象的常量指针,意味着不能把this指针绑定到一个常量对象上
    // 把this设置为指向常量的指针有助于提高函数的灵活性
    // 这种函数叫做常量成员函数
    string isbn() const
    {
        return bookNo;
        //bookNo相当于this-> bookNo
    }
	Sales_data& combine(const Sales_data&);
private:
    inline double avg_price() const;
    
	string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};

//定义在类外部的构造函数
//之前read提示找不到标识符,发现将非成员函数声明为类的友元函数就可以了,这样类成员变量修改为private也没关系
// 经验就是:成员函数调用非成员函数要通过firend声明
// 书上写,如果希望类的用户能够调用某个友元函数,就需要在友元声明之外对函数再次声明一次
// 上面不是必须的,这个和编译器有关系,经验就是把友元函数的声明与类的定义放在同一个头文件中
//Sales_data::Sales_data(istream &is)
//{
//	// 从is中读取交易信息存入this对象中
//	read(is, *this);
//}

// 定义在类外的成员函数类作用域运算符不可少
double Sales_data::avg_price() const {
    if (units_sold)
        return revenue/units_sold;
    else
        return 0;
}

// combine类似于"+"内置运算符,返回类型应该为可以被修改的左值,因此返回值必须为引用
// 调用一个返回引用的函数得到左值,其他返回类型得到右值
// *this解引用得到返回对象
Sales_data& Sales_data::combine(const Sales_data &rhs) {
    units_sold += rhs.units_sold;
    revenue += rhs.revenue;
    return *this;
}

//Sales_data的非成员数据接口
//返回值为副本而非引用
Sales_data add(const Sales_data&, const Sales_data&);
//IO类属于不能被拷贝的类型,因此我们只能通过引用来传递他们
//因为读取和写入的操作会改变流的内容,所以两个函数接受的是普通引用
ostream& print(ostream&, const Sales_data&);
istream& read(istream&, Sales_data&);


// 类如果有其他的构造函数,也应该定义默认构造函数,因为类在存在构造函数的情况下不会调用默认构造函数
// 这个类没有默认构造函数
class NoDefault {
public:
	NoDefault(int i)
	{
		var = i;
	}

	int var;
};

class C {
public:
	// 类C的默认构造函数
	// 由于NoDefault没有默认构造函数,因此只能显式调用NoDefault的带参构造函数初始化nodefault
	C(int i = 5) : nodefault(i)
	{

	}

	NoDefault nodefault;
};

// 静态成员相关的类
class Account
{
public:
	void calculate()
	{
		amount += amount * interestRate;
	}

	static double rate()
	{
		return interestRate;
	}

	static void rate(double);

private:
	string owner;
	double amount;
	static double interestRate;
	static double initRate();
};


void Account::rate(double newRate)
{
	interestRate = newRate;
}
double Account::interestRate = initRate();


int hight = 1;
class SC
{
public:
	bool isHightEnough(int height)
	{
		return height > ::hight; //::表示访问位于类外层的对象,
	}
private:
	int hight = 0;
	int width = 0;
};



#endif //CHAPTER7_SALES_DATA_H
//main.cpp
#include "Sales_data.h"
#include"Screen.h"
#include<iostream>
using std::istream;
using std::ostream;
#include<vector>
using std::vector;


// 输入信息包括ISBN、售出总数和售出价格
istream & read(istream &is, Sales_data &item)
{
	double price = 0;
	is >> item.bookNo >> item.units_sold >> price;
	item.revenue = price * item.units_sold;
	return is;
}

ostream& print(ostream& os, const Sales_data& item)
{
	os << item.isbn() << " " << item.units_sold << " " << item.revenue << " "
		<< item.avg_price() << endl;
	return os;
}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
	Sales_data sum = lhs;
	sum.combine(rhs);
	return sum;


}

int main() {

	// 定义对象时用对象名+括号即可,如果是默认初始化则不用加括号!!!
	Sales_data item1;
	print(cout, item1);
	Sales_data item2("isbn-12-1312", 5, 10.00);
	print(cout, item2);
	Sales_data item3("isbn-13-3212");
	print(cout, item3);
	//Sales_data item4(cin);
	//print(cout, item4);
	// 隐式转换test,没有发现有啥用
	string null_book = "isbn-13-3212";
	item3.combine(null_book);
	print(cout, item3);

	vector<Sales_data> Sales_datas{ Sales_data(" ", 0, 0) };
	// 错误版本,类内初始值必须以等号或者花括号表示
	//vector<Sales_data> Sales_datas(Sales_data(" ", 0, 0));

	Screen myScreen(5, 5, 'X');
	myScreen.move(4, 0).set('#').display(cout);
	cout << endl;
	myScreen.display(cout);
	cout << endl;

	C c;
	cout << c.nodefault.var << endl;

	// 类的静态成员相关
	double r = Account::rate();
	//double Account::interestRate = initRate();

    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值