C++ 小白 学习记录7

这篇博客深入探讨了C++中的类和对象,重点讲解了数据封装、构造函数的使用,包括默认构造函数、拷贝构造函数以及构造函数的委托。同时,介绍了友元函数的概念,以及如何在类之间建立友元关系。此外,还提到了类的静态成员和字面值常量类的特性。
摘要由CSDN通过智能技术生成

c++博大精深, 越看越觉得不会的太多. 回炉一下吧~ 大神们 请勿浪费时间~

第七章

基本思想是数据抽象和封装. 数据抽象依赖接口和实现分离的编程技术. 接口包括用户所能执行的操作. 实现则包括类数据成员,接口实现的函数体以及各种私有函数. 封装实现类的接口和实现的分离. 接口对类外暴露的方法, 至于其怎么实现的则通过封装把实现细节隐藏在类内.

定义抽象数据类型

string isbn()const { return bookNo; } // 此处的const修饰的是this, 而this 本身是const, 两个const就变成this存的地址不可修改, this指向的对象不可修改, 其等价于(伪代码)

string Sales_data::isbn(const Sales_data *const this){} c++允许把const放在成员函数的参数列表之后. 这种做法可以增加函数可接受的参数类型, 非常量可以定义常量.

因为编译器首先编译成员的声明, 然后才是成员函数体. 所以在声明成员函数体可以随意使用类中的其他成员而无需在意这些成员出现的次序.

如果在类外部定义函数, 如果声明的是常量成员函数, 则参数列表后必须明确指定const, 且成员名必须包含类名.

构造函数: 与类同名, 没有返回类型, 可以有多个.不能被声明为const. 创建类的一个const对象时, 直到构造函数执行完, 对象才真正取得const属性.

默认构造函数=合成的默认构造函数. 如果定义了其他构造函数,必须定义一个默认构造函数

  1. 编译器只有发现没有声明任何构造函数时才会合成默认构造函数
  2. 编译器有时候会初始化错误的默认值
  3. 如果类内又包含其余类, 则不能合成默认构造函数.

=default 编译器生成构造函数.

Sales_data(const std::string& s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p* n) {}  跟其余的有很大差别. 下划线部分叫做 构造函数初始值列表 

Sales_data(const string& s) :bookNo(s) {} , 其余类内成员以合成默认构造函数的方式隐式初始化. 等价于    Sales_data(const string& s) :bookNo(s),units_sold(0), revenue(0) {}

并非所有编译器 都支持类内初始值. 所以 最好还是把类内成员都显示初始化.

类的外部也可以定义构造函数.(这个有点太自由了)

拷贝 赋值和析构

访问控制与封装

访问说明符 public, private, friend.

struct 和class的区别: struct 在第一个访问说明符之前的成员都是public, class相反.

friend 声明的函数可以使用类内的私有成员变量

当提供一个类内初始值时, 必须以符号=或者{}表示

一个const成员函数如果以引用的形式返回*this,  则其返回类型将是常量引用, 其无法继续链式操作.

类的其他特性

inline函数 定义和实现必须在同一个文件

可变数据成员 mutable, 一个可变数据成员永远不是const,即使是const对象的成员.

类可以把声明和定义分开; 只有声明的类 叫不完全类型, 此种类型只能用于 定义指针, 声明参数,返回类型

类允许包含指向其自身的引用或指针.

如果一个类指定了友元类, 则友元类的成员函数可以访问该类所有成员(为何不通过对外的方法访问呢? ).

友元关系不能传递.

友元 可以授予某个类, 授予后友元类可以使用该类所有成员变量和成员函数

友元也可以只授予某个类的成员函数, 授予后 该友元函数 可以使用 类中的所有成员.

C++ primier P253, 7.32 定义你自己的Screen和Window_mgr,其中clear是Window_mgr的成员,是Screen的友元:

由于Window_mgr中有Screen对象,所以在Window_mgr之前要声明Screen(个人理解: 此时Window_mgr中的成员变量只需要知道Screen是一个什么样的类型,还不需要知道Screen的详细情况)。

因为clear中调用Screen成员,所以Screen要在clear定义之前定义(个人理解: 此时因为clear在定义时, 需要知道Screen究竟需要多大内存, 详情情况是什么样子的, 所以clear定义之前必须先定义Screen)。

因为Screen类中要声明友元函数clear,而clear是Window_mgr的成员函数,所以在Screen之前要定义Window_mgr(个人理解: 这个好理解, 原函数都不存在的话,自然不能声明为友元函数.)

在声明时, 所需要的类型可以是只声明,没有定义的类型. 在定义时, 必须是已经定义的类型.

#pragma once
#include <iostream>
#include<string>
#include<vector>

using namespace std;

class Screen;
class Window_mgr { // 首先定义Window_mgr
private :
	vector<Screen>	screens;
public:
	using ScreenIndex = vector<Screen>::size_type;

	void clear(ScreenIndex); // 声明clear方法, 需要使用Screen, 所以在Window_mgr 声明 Screen

	Window_mgr() = default;

};
class Screen {

	friend void Window_mgr::clear(ScreenIndex); // 声明clear 为友元
public:
	typedef std::string::size_type strSZType;
	Screen() = default;
	Screen(strSZType ht, strSZType wd, char c) : height(ht), width(wd), contents(ht * wd, c) {}
	Screen(strSZType ht, strSZType wd) :height(ht), width(wd) { contents = string(ht * wd, ' '); }
	char get()const { return contents[cursor]; }
	inline char get(strSZType ht, strSZType wd) const;
	Screen& move(strSZType r, strSZType c);
	void some_member() const;
	Screen& set(char);  
	Screen& set(strSZType, strSZType, char);
	Screen& display(ostream & os) { do_display(os); return *this; }
	const Screen& display(ostream & os) const { do_display(os); return *this; }
private:
	strSZType cursor = 0;
	strSZType height = 0, width = 0;
	string contents = "";
	mutable size_t access_ctr = 0; // 可变数据成员, 即使是在一个const对象内也可以被修改
	void do_display(ostream & os) const { os << contents; }
};

inline Screen& Screen::move(strSZType r, strSZType c) {
	strSZType row = r * width;
	cursor = row + c;
	return *this;
}
char Screen::get(strSZType r, strSZType c) const {
	strSZType row = r * width;
	return contents[row + c];
}
void Screen::some_member() const {
	++access_ctr;
}
inline Screen& Screen::set(char c) {
	contents[cursor] = c;
	return *this;
}
inline Screen& Screen::set(strSZType r, strSZType col, char c) {
	contents[r * width + col] = c;
	return *this;
}
void Window_mgr::clear(ScreenIndex i) { //定义 clear的实现
	Screen& s = screens[i];
	s.contents = string(s.height * s.width, ' ');
}

重载函数 时不同的函数, 想声明为友元函数必须每一个单独声明.

友元声明仅仅是声明了权限, 并非真正的声明

类的作用域

需要注意的是 返回类型由于是在类的作用域之外的,在类外定义成员函数体时, 此时返回值也需要使用类名作为作用域.

类的定义:

  1. 编译成员的声明
  2. 直到类全部可见后才编译函数体

一般来说 内层作用域可以重新定义外层作用域中的名字,即使是该名字已经在内层使用过. 而在类中, 如果成员使用了外层中的某个名字, 而该名字又代表某一类型时, 则类中不能重新定义该名字.

typedef double Money;
string bal;
class Account {
public:
	Money balance() { return bal; } // 此处使用的是类内的 Money bal, 而非外层的string bal
private:
	typedef double Money; // 此处的定义是错误, 但是ms c++的编译器并不报错
	Money bal;
};

名字查找

类内名字查找规律: 当前之前 -> 外层作用域 -> 报错

函数内名字查找规律: 函数内该名字之前 -> 函数所在的类 -> 函数定义之前的作用域

构造函数

如果没有在构造函数的初始值列表中显示的初始化成员, 则该成员将在构造函数体之前执行默认初始化.

如果成员是const, 引用, 或者某种没有默认构造函数的类时, 必须初始化. 初始化const或者引用类型的数据成员的唯一机会就是使用构造函数.

class ConstRef {
public:
	ConstRef(int ii); // 此种声明 会导致ci, ri无法初始化
private:
	int i;
	const int ci;
	int& ri;
};
ConstRef::ConstRef(int ii) { // ri 未提供初始值
	i = ii;
	ci = ii; // ci 无法赋值
	ri = i; 
}
ConstRef::ConstRef(int ii):i(ii), ci(ii),ri(ii) { // 可以采用此种定义
	i = ii;
	ri = i;
}

成员初始化的顺序

成员初始化的顺序与构造函数后面的成员初始化列表的顺序无关, 只跟成员定义的顺序有关.

委托构造函数, 构造函数调用自身或者其余的构造函数, 非常像 先调用父类的构造函数然后执行自身的代码

#include<string>
using namespace std;
class Sales_data {
public:
	Sales_data(string s, unsigned cnt, double price) :bookNo(s), units_sold(cnt), revenue(cnt* price) {}
	Sales_data() :Sales_data("", 0, 0) {}
	Sales_data(string s) : Sales_data(s, 0, 0) {}
	Sales_data(istream& is) : Sales_data() { read(is, *this); } // 先调用无参 然后 调用三个参数, 再然后执行 read
// 非常像 先执行父类的构造函数, 然后再执行自身的代码

private:
	string bookNo;
	unsigned units_sold;
	double revenue;
};

隐式的类类型转换

这个不太好解释但又能理解. 如果函数a( Type b), 如果Type 有一个构造函数能够接收 比如 int a , 那么 就可以执行 a( int a), 此时int a 可以自动隐式转化为 Type b. 但是这种转换只能自动执行一步.需要特别注意的是, 转换的过程中生成的一个临时的对象, 使用过后就会被抛弃.

explicit  声明该构造函数不可隐式转换, 则上面的转换将会失效. 该关键字 只对 只有一个实参的构造函数有效. 需要多个实参的构造函数因为不能隐式转换, 无需该参数修饰.

explicit 只能在类内声明构造函数时使用, 类外定义时不应重复.

不能将explicit的构造函数用于拷贝形式的初始化, 即不能用 =, 可以直接初始化. 可以使用static_cast<type>(arg) 显式的转化.

尽量不要隐式转化吧, 太乱了

聚合类

  • 所有成员都是public
  • 没有定义任何构造函数
  • 没有类内初始值
  • 没有基类, 也没有virtual函数

聚合类可以使用{}, 依据定义顺序初始化类内成员. 跟数组一样.

字面值常量类

数据成员是字面值类型的聚合类 是 字面值常量类, 如果不是聚合类并且能够符合以下要求, 也是字面值常量类:

  • 数据成员必须是字面值类型
  • 必须至少包含一个constexpr构造函数
  • 数据成员有初始值,则初始值必须是一条常量表达式, 成员属于某种类类型, 初始值必须使用成员自己的constexpr构造函数
  • 必须使用析构函数的默认定义.

书中P268 在vs2019中报错 参照 https://blog.csdn.net/qq_34801642/article/details/104634709 的文章 可以知道 c++ 版本区别造成.

修改vs2019 c++版本:

此处最低为14啊, 怎么改呢?

静态成员

静态变量和静态函数 捕鱼任何对象绑定在一起, 可以使用类和作用域访问符调用静态对象/函数, 也可以使用对象/引用/指针访问静态成员.

既可以在类内定义也可以在类外定义, static只能在类内出现一次. 必须在类的外部定义和初始化每个静态成员. 只能定义一次.

为确保对象只定义一次, 最好是把静态数据成员的定义与其他非内联的函数定义 放在同一个文件中.

即使一个常量静态数据成员在类内部被初始化了, 通常情况下也应该在类的外部定义一下该成员.

静态成员可以是不完全类型.

静态数据成员的类型可以就是它所属的类类型, 而非静态数据成员只能使用所属类的指针或引用.

静态成员可以作为默认实参.

//7.58
#include<vector>
using namespace std;
class Example {
public:
	static double rate;  // 类内声明, 类外定义
	static constexpr int vecSize = 20;
	static vector<double> vec;

};
double Example::rate = 6.5;
vector<double> Example::vec(Example::vecSize);

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yutao1131

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值