Re: ゼロから学ぶ C++ 入門(九)类和对象·最终篇上:缓冲区同步与流绑定、取地址运算符重载、const成员函数、初始化列表

◆ 博主名称: 晓此方-CSDN博客

大家好,欢迎来到晓此方的博客。

⭐️C++系列个人专栏:

Re:从零开始的C++_晓此方的博客-CSDN博客

⭐️踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰


目录

0.1概要&序論

一,缓冲区同步与流绑定

1.1缓冲区与缓冲区刷新

1.1.1缓冲区的意义

1.1.2缓冲区的定义

1.1.3刷新缓冲区的定义

1.1.4刷新缓冲区的时机

1.2C/C++缓冲区同步刷新

1.2.1同步刷新的缺陷

1.2.2解决同步缺陷的办法

1.3输入输出流绑定

1.3.1C-library官方文档摘要

1.3.2单向绑定

1.3.3流绑定去缺陷

二,取地址运算符重载

[2.1const 成员函数](#2.1const 成员函数)

2.1.1const成员函数定义

2.1.2const成员函数的意义

2.2取地址运算符重载

2.2.1取地址运算符重载的定义

2.2.2取地址运算符重载的类型

2.2.3取地址运算符重载的使用

三,再谈构造函数---初始化列表

3.1初始化列表的定义

3.2初始化列表的使用

3.3成员变量的初始化

3.4初始化列表的重要性

3.4.1const类型成员变量

3.4.2引用类型成员变量

3.4.3没有默认构造函数的自定义类型成员变量

3.5成员缺省值(C++11新增)

3.5.1成员缺省值的定义

3.5.2成员缺省值的概念混淆点(个人总结)

3.6一切成员变量必须经过初始化列表

3.6.1"对象构造之时"

3.6.1.1"对象构造之时"定义

3.6.1.2对象定义+构造三大步骤

3.6.2初始化列表没有显示实现并不代表初始化列表不存在

3.6.3原则与总结

3.7初始化列表的初始化顺序

3.7.1一道易错题引入

3.7.2原因与原理

3.8初始化列表最终总结


0.1概要&序論

这里是此方,久しぶりです!本篇是类和对象的最终篇上,C++前期最困难的部分终于要熬过去了!本文会解构构造函数剩下的伏笔------构造函数的初始化列表以及运算符重载的最后一站:取地址运算符重载,以及缓冲区同步与流绑定的秘密内容干货满满「此方」です。让我们现在开始吧!

一,缓冲区同步与流绑定

1.1缓冲区与缓冲区刷新

1.1.1缓冲区的意义

磁盘、屏幕、网络等I/O设备的速度相比CPU极慢,如果每一次输出、每一个字符、每一次读写,都让CPU停下来等待I/O设备,实际上会极大程度的拖慢运行效率,因此缓冲区就诞生了

1.1.2缓冲区的定义

缓冲区是用于暂存输入/输出给IO设备的数据 ,并在刷新缓冲区 的时候统一输入输出以提升效率的位于程序与 I/O 设备之间的中介层

1.1.3刷新缓冲区的定义

刷新缓冲区是主动或被动 触发的一次强制 I/O 行为 ,用于清空或同步 缓冲区中的待处理数据。

1.1.4刷新缓冲区的时机

刷新缓冲区的时机有很多种,以下列举常见情况:

1、缓冲区满(最常见)。

2、遇到刷新指令如换行endl,\n。

3、从输出转向输入(连续两条输出/输入语句不行)。

cpp 复制代码
cout << "请输入:";
cin >> x;

4、程序正常结束。

1.2C/C++缓冲区同步刷新

C++兼容C语言,刷新C++的缓冲区的时候也会刷新C语言的缓冲区 。如下:这两条语句可以共存,在执行cin>>a之前,会刷新C的缓冲区,让printf()打印出来

cpp 复制代码
printf("%d",123);
cin>>a;

1.2.1同步刷新的缺陷

但是这种同步存在缺陷,C++ 的 iostream 在默认设计 中,被要求"即使没写 C 代码,也要假设可能会用到 C 的 stdio**"**,所以它必须主动维护一致性。C++为了兼容C的这种操作本质上降低了运行效率。

1.2.2解决同步缺陷的办法

cpp 复制代码
ios_base::sync_with_stdio(false);

该代码的目的是**关闭 C++ iostream 与 C stdio之间的同步机制。**可在一些高IO需求同时不使用C/C++混合代码的领域使用。

1.3输入输出流绑定

1.3.1C-library官方文档摘要

cin is tied to the standard output stream cout(see ios::tie), which indicates that cout's buffer is flushed (see ostream::flush) before each i/o operation performed on cin.

这段文字解释的是:cin 默认绑定(tie)到 cout ,这意味着在每次对 cin 进行输入操作之前,cout 的缓冲区都会被强制刷新。

1.3.2单向绑定

如下代码:在执行cin之前,cout的buffer(缓冲区)会被强制刷新, 让"请输入:"打印在屏幕上。 但是,如果交换两者的位置 ,也就是输入流在前,输出流在后,输出流发生时不会刷新输入流的缓冲区。

cpp 复制代码
cout << "请输入:";
cin >> x;

有人可能认为,这样不就乱套了?不刷新出去缓冲区中既有输出流的数据又有输入流的数据。

实际上,输入流和输出流的缓冲区相互独立。互不影响

1.3.3流绑定去缺陷

cout和cin的绑定实际上也带来了一些效率上的问题。在仅使用cin输入的时候,cin任然会在每一次调用时检查cout缓冲区导致了效率上的缺陷

解决办法: 关闭输入输出流与其他所有流的绑定,代码如下:

cpp 复制代码
cin.tie(nullptr);
cout.tie(nullptr);

二,取地址运算符重载

2.1const 成员函数

2.1.1const成员函数定义

将 const 修饰的成员函数称之为 const 成员函数,const 修饰成员函数放到成员函数参数列表的后面。

cpp 复制代码
void print()const

const 实际修饰该成员函数隐含的 this 指针 ,表明在该成员函数中不能对类的任何成员进行修改 。隐含的 this 指针由 Date* const this 变为 const Date* const this。

这里稍微解释一下:

原本Date* const this, 是指this指针本身不可修改。经过const修饰后的const Date* const this指的是this指针指向的内容不可修改。

2.1.2const成员函数的意义

被创建对象有普通对象,也有const对象,当const对象调用普通成员函数时:就相当于将一个const类型的对象指针传递给一个非const的this指针,引发权限放大的危险。

要把权力关进只读(制度)的笼子里

这里有一个语法设计的问题:"即使函数自身并没有修改该const对象 ,比如print()函数,**为什么任然禁止使用非const函数访问const类型对象,**也就是说实际上并没有权限放大,为什么还是会报权限放大的错误"

直接给出结论:

const 对象只能调用 const 成员函数,不是因为函数"实际上有没有修改对象",而是因为函数"有没有被声明为不修改对象"。编译器只看类型签名,不看函数实现。

日后我们在写代码的时候,如果遇到函数内部不改变成员变量的,建议加上const修饰 ,这样,该函数就可以传递任何值作为参数 了。(包括const对象和非const对象

2.2取地址运算符重载

2.2.1取地址运算符重载的定义

一般取地址运算符重载编译器自动生成的就够我们用了,不需要去显示实现 。除非一些很特殊的场景:比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。

2.2.2取地址运算符重载的类型

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载

为甚么有两个版本?就是一个优先级的问题:

const对象应当返回const指针,一般对象应当返回一般指针,为了为不同的对(const与非const )调用提供不同对应的版本,编译器会自动匹配调用最合适的版本。

如果用户显式声明了取地址运算符重载 ,编译器将不会再自动生成默认版本 ,因此通常需要同时提供 const 与非 const 两个版本,否则可能导致某些对象无法取地址

cpp 复制代码
Date* operator&()
{
    return this;
    // return nullptr;
}

const Date* operator&() const
{
    return this;
    // return nullptr;
}

**80%的读者看到这里一定会产生疑惑:**如果只写了一个const版本的取地址重载,为什么一般对象无法通过权限缩小来调用它?

C++ 只在"值的使用"层面允许权限缩小 ,在"对象身份 / this 绑定"层面,绝对不允许。

(这里涉及重载决议等复杂内容,暂时不太好讲。不过只需要记住这一种特殊情况:取地址运动算符重载是唯数不多不支持权限缩小调用的

2.2.3取地址运算符重载的使用

cpp 复制代码
Date* operator&()
{
    //return this;
    //return nullptr;
    return 0x2673FF32;
}
  1. 可以返回一个nullptr空指针,让使用者认识到该对象的指针不能被找到。
  2. 可以返回一个随意的指针,让使用者无法察觉到该对象的指针不能被找到,同时又不能利用该指针

那么我们这么搞不会产生类似于野指针的问题吗?

**结论:**实践当中是没有这种需求的。😂

三,再谈构造函数---初始化列表

3.1初始化列表的定义

之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值 ,构造函数初始化还有一种方式,就是初始化列表。

3.2初始化列表的使用

初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表 ,每个"成员变量"后面跟一个放在括号中的初始值或表达式 。(注意:可以是各种表达式

cpp 复制代码
Date(int year = 1, int month =1,int* ptr )
    : _year(year)
    , _month(month+1)
    , ptr(malloc(sizeof(int)))
{}

初始化变量比较少的时候会写成一行:(这两种方式大体上是没有区别的)

cpp 复制代码
Date(int& x, int year = 1, int month =1)
    : _year(year), _month(month+1)
{}

此外,初始化列表可以和函数体共存。

cpp 复制代码
Date(int* ptr )
    :ptr(malloc(sizeof(int))){
   if(ptr==nullptr){
       perror("malloc:fail");
       eixt(-1);
   }
}

3.3成员变量的初始化

语法理解上初始化列表可以认为是每个成员变量初始化的地方因此: 由于成员变量初始化只能发生一次一个成员变量在初始化列表只能出现一次

cpp 复制代码
error C2437:  "year" : 已初始化

看到这里,读者可能会感到糊涂,所以我们重新捋一捋一个成员变量的一生

3.4初始化列表的重要性

先说结论:

引用类型成员变量、const 成员变量、没有默认构造函数的自定义类型成员变量,必须放在初始化列表位置进行初始化,否则会编译报错。

3.4.1const类型成员变量

const成员变量必须在该变量初始化时完成初始化,初始化列表正是这一初始化发生的位置(调用构造函数初始化列表是const类型成员变量初始化的唯一方式);构造函数体内只能进行赋值(先别急,这个接下来会讲),而const成员不允许赋值。

cpp 复制代码
class Date{
public:
    Date()
        : _n(1)
private:
    const int _n;
};
int main(){
    Date d1();
}

3.4.2引用类型成员变量

引用成员变量必须在初始化时完成绑定, 并且引用不初始化在语法层面禁止,初始化只能在初始化列表中完成(调用构造函数初始化列表 也是引用类型成员变量初始化的唯一方式**)** ;构造函数体内不存在任何合法方式可以再处理它

cpp 复制代码
class Date{
public:
    Date(int& xx)
    : _ref(xx){}
private:    
    int& _ref;
};
int main(){
    int x = 0;
    Date d1(x);
}

如上代码: 传递x给xx,再将xx作为初始化值传递给**_ref完成引用的初始化**,

3.4.3没有默认构造函数的自定义类型成员变量

cpp 复制代码
class Time{
public:
     Time(int hour)
     :_hour(hour){}
private:
     int _hour;
};
class Date{
     public:
     :_t(1){}
private:
     Time _t;          
};
int main()
{
     Date d1;
     return 0;
}

对于自定义类型成员变量,编译器要求调用这个成员变量的默认构造 函数初始化。如果这个成员变量没有默认构造函数,那么就会报错。为了解决这个问题: 必须通过初始化列表间接传参完成初始化。代码如上,在初始化列表中 :_t(1)传递值1给_t的非默认构造函数,实现初始化。

如果该自定义类型的成员变量有多个 ,可以这样传递:(将一个数集传递

cpp 复制代码
 :_t({1,2})

事实上也可以这么看: 初始化列表的这个操作:_t(1) 就是一次构造函数调用。 但它是一种特殊语境下的构造函数调用 ,和常见的在普通表达式里写Time _t(1) 在语义上等价.

而当成员类型存在默认构造函数,且该成员未出现在构造函数初始化列表中时,编译器会在对象构造阶段自动对该成员执行默认构造

3.5成员缺省值(C++11新增)

3.5.1成员缺省值的定义

成员缺省值又名成员默认值、默认成员初始值C++11 支持持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员 以及默认生成的默认构造函数

成员缺省值除了给值还可以给各种表达式:

cpp 复制代码
private:
    int* _ptr = (int*)malloc(12);
};

3.5.2成员缺省值的概念混淆点(个人总结)

一,成员缺省值这样写还是声明,不是定义 !定义一定要开辟空间!这样写后面的值是成员缺省值 。如果没有显示在初始化列表初始化的成员,那么就会用缺省值进行初始化

二,有缺省值不代表这个构造函数是默认构造函数了默认构造函数就是指不传参就可以调用的构造函数我们称之为默认构造函数。这个构造函数任然需要传递参数调用,所以就不是默认构造函数。

三,成员缺省值与函数缺省参数会不会冲突?

先说结论:

缺省参数和成员缺省值 ,两者看起来都叫"缺省",但根本不是同一类机制

构造函数缺省参数是的使用满足两个条件:

  • 在这个函数被调用的时候没有传递相应的实参值给这个函数对应的形参。
  • 在调用这个函数的时候该成员变量在初始化列表或者函数体内使用了该形参参数。

而成员缺省值是:

只有在该成员变量在初始化列表里根本就不存在没有显示写这个成员的初始化 )的时候,才使用成员缺省值对该成员变量初始化(算是一种保底的初始化方式)(对内置类型直接使用成员缺省值初始化,将该缺省值视为初始化参数,直接调用匹配的构造函数完成构造。)

缺省参数复习传送门:
Re:从零开始学C++(一)基础精讲·上篇:命名空间、输入输出、缺省参数、函数重载-CSDN博客https://blog.csdn.net/Z2314246476/article/details/155713821?spm=1001.2014.3001.5501

3.6一切成员变量必须经过初始化列表

3.6.1"对象构造之时"

以下内容将颠覆你对初始化的认知

3.6.1.1"对象构造之时"定义

"对象构造之时"指的是------在对象存储空间已经确定之后、构造函数(含初始化列表)执行的那一整个阶段。 它既不是"只开空间",也不是"只调用构造函数",而是二者按严格顺序组成的一次事件

3.6.1.2对象定义+构造三大步骤
  1. 对象开辟空间
  2. 对象成员变量经过初始化列表实现初始化
  3. 对象成员变量经过构造函数函数函数体赋值
cpp 复制代码
class A {
public:
    A() {
        x = 10;
    }
private:
    int x;
};

试着回答一下:**成员变量x在哪里实现初始化?在函数体内:x = 10?**事实上这并不正确。

正确的事实:

  • X在进入构造函数体之前经对象定义开辟空间。
  • X经过构造函数初始化列表完成初始化步骤。
  • X=10 是赋值,不是初始化。

如果有成员缺省值:

  • 在初始化阶段,经过初始化列表,如果没有显示写某个成员变量的初始化值。就会使用成员缺省值进行初始化。

3.6.2初始化列表没有显示实现并不代表初始化列表不存在

也就是说:你看到的函数和编译器看到的实际上不是一回事(如下) 。任何成员变量调用构造函数都会经过初始化列表。

cpp 复制代码
A() : x(/* 什么也没做 */) {
    x = 10;
}

3.6.3原则与总结

原则: 尽量使用初始化列表初始化**,因为那些你不在初始化列表初始化的成员也会走初始化列表。**
总结

如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化 。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,(C++并没有规定。 )对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误

3.7初始化列表的初始化顺序

3.7.1一道易错题引入

下面程序的运行结果是什么( )

A. 输出 1 1
B. 输出 2 2
C. 编译报错
D. 输出 1 随机值
E. 输出 1 2

cpp 复制代码
#include <iostream>
using namespace std;
class A{
public:
    A(int a)
        : _a1(a)
        , _a2(_a1){}
    void Print(){
        cout << _a1 << " " << _a2 << endl;
    }
private:
    int _a2 = 2;
    int _a1 = 2;
};
int main(){
    A aa(1);
    aa.Print();
}

答案是:D

3.7.2原因与原理

初始化列表中按照成员变量在类中声明顺序进行初始化 ,跟成员在初始化列表出现的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。

声明的顺序就是成员变量的内存的存放顺序

如图:_a2的声明在_a1前面,所以在内存中的存放位置也在_a1前面。因此初始化时先初始化_a2再初始化_a1。

3.8初始化列表最终总结

每个成员都要走初始化列表

1、在初始化列表初始化的成员(显示写

a、内置类型:直接初始化

b、自定义类型:本质上调用其构造函数

2、没有在初始化列表的成员(不显示写

a、声明的地方有缺省值,用缺省值

b、没有缺省值

x:内置类型,不确定,看编译器,大概率是随机值

y:自定义类型,调用默认构造,没有默认构造就编译报错

3、引用 / const / 没有默认构造的自定义类型:必须在初始化列表初始化


好了本期内容就到这里,如果对你有帮助,不要忘记点赞三联一波哦,我是此方,我们下期再见!

相关推荐
hetao17338372 小时前
2025-12-30 hetao1733837 的刷题笔记
c++·笔记·算法
前端不太难2 小时前
RN 列表里的局部状态和全局状态边界
开发语言·前端·harmonyos
程琬清君2 小时前
前端动态标尺
开发语言·前端·javascript
技术净胜2 小时前
Python常用框架介绍
开发语言·python·sqlite
曹轲恒2 小时前
jvm 局部变量表slot复用问题
java·开发语言·jvm
222you2 小时前
SpringMVC的单文件上传
java·开发语言
小徐不会敲代码~2 小时前
Vue3 学习 6
开发语言·前端·vue.js·学习
yaoxin5211232 小时前
277. Java Stream API - 去重与排序:Stream 中的 distinct() 与 sorted()
java·开发语言
k***92162 小时前
C语言模拟面向对象三大特性与C++实现对比
java·c语言·c++