C++Primer学习笔记:第7章 类

  • 类的基本思想是数据抽象data abstraction和封装encapsulation。数据抽象是一种依赖于接口interface和实现implementation分离的编程技术

  • 在类中,由类的设计者负责考虑类的实现过程,使用该类的程序员只需要抽象地思考类型做了什么,而无须了解类型的工作细节

  • 执行加法和IO的函数应该定义成普通函数,执行复合赋值运算的函数应该是成员函数

  • 成员函数的声明必须在类的内部,它的定义则既可以在类的内部也可以在类的外部

  • 成员函数通过一个名为this的额外的隐式参数来访问调用它的对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化该常量指针this。因此我们不能自定义名为this的参数或者变量

  • 紧随参数列表之后的const关键字修改隐式this指针为指向类类型常量版本的常量指针(默认不是指向常量的)。这意味着如果一个成员函数是普通函数(函数参数列表后没有const),则一个常量类对象无法调用该函数(无法给this指针初始化)

  • 如果成员函数内部对对象的其他成员没有修改,则尽可能将成员函数声明为常量成员函数,紧跟在函数参数列表后面的const表示this是一个指向常量的常量指针。将函数声明为常量成员函数有利于提高函数的灵活性,令常量对象仍然可以调用该函数

  • 常量对象,以及常量对象的引用或者指针都只能调用常量成员函数

  • 类本身就是一个作用域,编译器分两部处理:首先编译成员的声明,然后才轮到成员函数体。成员函数体可以随意使用类中的成员而无须在意这些成员出现的次序

  • 类外部定义的成员的名字必须包含它所属的类名,而且返回类型、参数列表和函数名都得与类内部的声明保持一致

    double Sales_data::avg_price() const 
    {}
    
  • 当我们定义函数类似于某个内置运算符时,应该令函数的行为尽量模仿这个运算符

  • 如果函数在概念上属于类但是不定义在类中,则它一般应该与类声明在同一个头文件中

  • 默认情况下,拷贝类的对象其实是拷贝对象的数据成员

  • 构造函数没有返回类型,构造函数不能被声明为const,当我们创建类的一个const对象时,直到构造函数完成初始化过程对象才真正取得常量属性

  • 如果我们的类没有显式地定义构造函数,则编译器会为我们隐式定义一个默认构造函数。默认构造函数没有任何实际参数。这个编译器创建的构造函数又称作合成的默认构造函数

    • 如果存在类内的初始值,用它来初始化成员
    • 否则,默认初始化该成员
  • 不能依赖默认构造函数:

    • 一旦我们定义了构造函数(不论是否有参),除非我们自己定义一个默认构造函数(无参),否则类将没有默认构造函数。只有当类没有声明任何构造函数的时候编译器才会自动生成默认构造函数
    • 如果定义在块中的内置类型或复合类型(数组和指针)的对象被默认初始化,则其值是未定义的。除非他们都有类内初始值
    • 如果类中包含一个其他类,而且这个类没有默认构造函数,那么编译器将无法初始化该成员
  • 如果函数定义在类内部,则函数默认是内联的,如果在外部,则默认是不内联的

  • 在C++11新标准中,我们可以通过在参数列表后面写上=default来要求编译器生成构造函数(在类外仍然可以)

  • 在构造函数中可以使用构造函数初始值列表对部分成员进行初始化,在初始值列表初始化后用类内初始化对成员进行初始化,剩下的执行默认初始化,然后再执行构造函数函数体中的内容

  • 如果我们不定义拷贝、赋值和析构操作,则编译器会会替我们合成。一般来说,编译器生成的版本将对对象中的每个成员执行拷贝、赋值和销毁操作

    • 拷贝:初始化变量、以值的方式传递或返回一个对象
    • 赋值:使用赋值运算符
    • 销毁:当对象不再存在时销毁
  • 每个访问说明符指定了接下来成员的访问级别,其有效范围直到出现下一个访问说明符或者到达类的结尾为止

    • 定义在public说明符之后的成员在整个程序内可以被访问,定义类的接口
    • 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问
  • classstruct的唯一区别:默认访问权限不同,struct的默认访问权限是publicclass的默认访问权限是private

  • 类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可。友元声明只能出现在类定义的内部,但是具体位置不限。一般来说,最好在类定义开始或结束前的位置集中声明友元

  • 友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明,我们必须在友元声明之外再专门对函数进行一次声明(一些编译器允许不再次声明,不过为了能够让所有的编译器成功运行,最好还是加上)。为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中。

  • 除了定义数据和函数成员外,类还可以定义某种类型在类中的别名,由类定义的类型名字和其他成员一样存在访问限制

    class Screen 
    {
    public:typedef std::string::size_type pos;//using pos = std::string::size;
    }
    

    用来定义类型的成员必须先定义后使用,这一点与普通成员有所区别。因此类型成员通常出现在类开始的地方

  • 如果我们希望无论如何都能够修改某个类的数据成员,即使该对象是const,我们可以在变量的声明中加入mutable关键字,称作可变数据成员。可以推测,一个可变数据成员无论如何都不是const的。

  • 当我们提供类内初始值时,必须以符号=或者花括号表示,不能用圆括号

  • 如果想要在成员函数中返回对象本身,则返回类型应该为引用,返回值为*this

  • 一个const成员函数如果以引用的形式返回*this,则应该返回一个常量引用

  • 我们可以通过成员函数是否是const对函数进行重载,其原因如同函数的指针参数是否指向const可以进行重载一样。如果一个函数既有const版本,又有非const版本,则对于常量对象仅仅可以调用const版本,对于非常量对象非const版本显然是一个更好的匹配

  • 在类内使用一些小函数不会增加运行时的额外开销,相反还会给开发带来很多好处

    class Screen
    {public:Screen &display(std::ostream &os) {do_display(os); return *this;}const Screen &display(std::ostream &os) const {do_display(os); return *this;}void do_display(std::ostream &os) const {os << contents;}
    }
    
  • 每个类定义了唯一的类型,对于两个类来说,即使他们的成语完全一样,这两个类也是完全不同的类型

  • 在C语言中要求类类型定义对象时加上classstruct,但是C++中不必要

  • 我们也可以仅仅声明类而不定义它,这种声明有时被称作前向声明,在定义之前是一个不完全类型:可以定义指向这种类型的指针或者引用,也可以声明(不能定义)以不完全类型作为参数或者返回类型的函数。因此,一个类的名字出现过以后,就被认为是声明过了,允许包含指向它自身类型的引用或者指针

  • 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。友元关系不具有传递性

  • 需要注意的是,友元的声明仅仅影响的是该函数的访问权限,因此并不能看作声明,为了完成对该友元的调用,必须在其作用域内对友元进行声明。友元的作用域和类的声明是同一级别的

    struct X
    {friend void f()	{}	//友元函数可以定义在类的内部,但是此时在类X的作用域中是不可见的X() { f(); }		//错误,f()不可见void g();void h();
    }
    void X::g() { f(); }	//错误,f()不可见
    void f();				//对f进行声明,此时对X可见
    void X::h()	{ f(); }	//正确
    

    需要注意的是,有的编译器对上面没有这种限制

  • 定义在外部的成员函数的返回类型不在类的作用域内

  • 类的定义分两步处理:

    • 顺序编译成员的声明
    • 指导类全部可见后编译函数体
  • 一般来说,内层作用域可以重新定义外层作用域名字,即使该名字已经在内层作用域中使用过,而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表某一种类型,则类不能重新定义该名字(一些编译器忽略这种错误)

  • 类型名的定义通常出现在类的开始处,这样保证所有使用该类型的成员都出现在类型之后

  • 尽管全局变量有可能被覆盖掉,但是我们可以使用::name来获取对应的全局变量

  • 当成员定义在类的外部时,名字查找的最后一步不仅要考虑类定义之前的全局作用域中的声明,还要考虑在成员函数定义之前的全局作用域中的声明

  • 如果没有在构造函数的初始值列表中显式地初始化成员,而且该成员没有类内初始值,则该成员将在构造函数体之前执行默认初始化。有时候有的变量无法进行默认初始化(如一些没有默认构造函数的类对象,以及引用或者常量)

  • 在构造函数初始值中每个成员只能出现一次。构造函数初始值列表只用于说明初始化成员的值,而不限定初始化具体执行顺序,成员的初始化顺序与他们在类定义中的出现顺序一致,构造函数初始值列表中初始值的前后位置不会影响实际的初始化顺序。如果使用某些成员初始化其他成员,则初始化的顺序就比较重要,不过这种做法不是一个好的编程习惯

  • 如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数

  • 我们可以使用委托构造函数,委托其他构造函数进行构造

    class X
    {X(int _a, int _b, int _c):a(_a),b(_b),c(_c){}X():X(0, 0, 0){}X(int _a):X(_a, 0, 0){}X{int _a, int _b):X(_a, _b, 0){}
    };
    

    委托构造函数会先执行被委托构造函数的初始化列表,然后再执行函数体

  • 当对象被默认初始化或值初始化时自动执行默认构造函数

  • 默认初始化在以下情况下发生:

    • 当我们在块作用域内不适用任何初始值定义的一个非静态变量或者数组时
    • 当一个类本身含有类类型的成员且使用合成的默认构造函数时
    • 当类类型的成员没有在构造函数初始值列表中显式地初始化时
  • 值初始化在以下情况下发生:

    • 在数组初始化的过程中如果我们提供的初始值的数量小于数组的大小时
    • 当我们不使用初始值定义一个局部静态变量时
    • 当我们通过书写形如t()的表达式显式地请求值初始化时
  • 在实际中,如果提供了其他构造函数,最好也提供一个默认的构造函数

  • 如果想定义一个使用默认构造函数进行初始化的对象,正确的方法是去掉对象名后的空的括号对

    sales_data obj();    //声明了一个函数
    sales_data obj1;     //声明了一个对象,使用默认构造函数进行初始化
    
  • 能通过一个实参调用的构造函数定义了一条从构造函数参数类型向类类型隐式转换的规则,但是这种转换只允许一步

  • 我们可以通过将构造函数声明为explicit阻止隐式转换,只对一个实参的构造函数有效。只能在类内声明构造函数时使用explicit关键字,在类外定义时不应重复。explicit构造函数只能用于直接初始化

  • 我们可以使用explicit构造函数进行强制类型转换

    item.combine(static_cast<sales_data>(cin));
    
    • const char * -> string的构造函数不是explicit
    • 接收一个容量参数的vector构造函数是explicit
    • 需要注意的是**explicit阻止的隐式转换是构造函数参数类型转换为类类型的转换**,而不是构造函数参数之间的转换
    • 允许临时量存在的地方才允许隐式类型转换
    • 聚合类使得用户可以直接访问其成员,因此具有特殊的初始化语法形式,当一个类满足如下条件时,我们说它是聚合的
      • 所有成员都是public
      • 没有定义任何构造函数
      • 没有类内初始值
      • 没有基类,也没有virtual函数
        例如:
      struct Data
      {int i; string s;
      };
      Data v1 = {0, "1"};
      
      我们可以提供一个花括号括起来的成员初始值列表用来初始化,当然顺序要和声明顺序一致。
  • 数据成员那都是字面值类型的聚合类是字面值常量类。或者满足以下要求:

    • 数据成员必须是字面值类型
    • 类必须至少含有一个constexpr构造函数
    • 如果某个成员有类内初始值,内置类型必须是一条常量表达式,类类型必须使用自己的constexpr构造函数
    • 类必须使用析构函数的默认定义
  • 我们可以通过在成员的声明之前加上关键字static使得其与类关联在一起。类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。因为静态函数成员中没有this指针),静态成员啊还是那话不能声明成const,也不能调用非静态成员

  • 可以使用作用域运算符直接访问静态成员。虽然静态成员不属于某个对象,但是我们还是可以使用类的对象、引用或者指针访问静态成员。成员函数不通过作用域运算符就能直接访问静态成员

  • 静态成员的static关键字只能在类内部出现(同explicit),不能在类外重复使用

  • 静态成员不能由类的构造函数初始化,静态数据成员定义在任何函数之外,因此一旦被定义就存在于程序的整个生命周期中。一般来说,我们不能在类的内部初始化静态成员,相反的,必须在类的外部定义和初始化每个静态成员

  • 通常情况下,类的静态成员不应该在类的内部初始化。然而我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。这样的静态常量可以用在所有适合用于常量表达式的地方。即使该成员在类内已经初始化,还是应该在类外定义一下该成员,而且不能再指定一个初始值了。这样做的好处是能够在类的外部使用该静态成员。

    void func(const int &x)
    {cout << "x:" << x << endl;
    }
    struct A
    {static const int a;static constexpr int aa = 6;static vector<int> v;
    };
    vector<int> A::v(aa);
    const int A::a = 5;
    int main()
    {//const A a;//a.test();cout << A::v.size() << endl;                                                                                                                          func(A::a);return 0;
    }
  • 静态成员能够用于某些场景而普通成员不能:

    • 静态数据成员可以是不完全类型,特别的,静态数据成员可以是它所属的类类型,而非静态数据成员则收到限制,只能声明成它所属类的指针或引用
    • 静态成员和普通成员的另外一个区别是我们可以使用静态成员为默认实参

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/383563.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

每日一题:leetcode191.位1的个数

题目描述 题目分析 很自然地想到了二进制枚举&#xff0c;直接循环检查每一个二进制位。 class Solution { public:int hammingWeight(uint32_t n) {int ret 0;uint32_t t 1;for (int i 0; i < 32; i, t << 1) {if (n & t) {ret;}}return ret;} };AC之后看了…

每日一题:leetcode341.扁平化嵌套列表迭代器

题目描述 题目分析 这个题目自己大概花了一个小时&#xff0c;虽然是一遍AC&#xff0c;但是速度有点慢&#xff0c;太长时间不写代码导致自己对代码不太敏感&#xff0c;写起来慢腾腾的。 看到这个的想法就是&#xff0c;要用栈来保存列表的迭代器&#xff0c;这样将孩子列表…

每日一题:leetcode82. 删除排序链表中的重复元素 II

题目描述 题目分析 这才是正常的中等题难度嘛&#xff0c;昨天的中等题题解我半天看不懂。。。 首先&#xff0c;需要增加一个哑节点&#xff08;操作链表的常规操作&#xff09;&#xff0c;因为有可能删除首节点&#xff0c;我们不想要为首节点添加单独的逻辑。其次&#xf…

每日一题:leetcode456.132模式

题目描述 题目分析 我觉得这道题应该是我做过最难的中等题之一了&#xff0c;这是昨天的每日一题&#xff0c;但是昨天用nlogn的做法做出来以后在看题解&#xff0c;发现有些看不懂&#xff08;觉得题解有点故弄玄虚&#xff09;。然后今天中午又花了一点时间才搞懂&#xff0…

leetcode283.移动零

题目描述 题目分析 在写简单题放松&#xff0c;看到这道题第一个想法是用STL库函数&#xff0c;虽然知道大概要用双指针之类的&#xff0c;但是库函数爽哇。 class Solution { public:void moveZeroes(vector<int>& nums) {stable_sort(nums.begin(), nums.end(), …

每日一题:leetcode61.旋转链表

题目描述 题目分析 很容易发现&#xff0c;如果k是n的整数倍&#xff0c;相当于没有移动。这样直接对k%n使得k在一个可以接受的范围。 因为是顺序移动&#xff0c;各元素之间的相对位置保持不变&#xff0c;所以就想着将链表先变成一个环。然后再移动头指针&#xff0c;最后再…

每日一题:leetcode173.二叉搜索树迭代器

题目描述 题目分析 更加地觉得编程重要的不在于如何写代码&#xff0c;用什么具体的技巧&#xff0c;编码本身只是一种将思维呈现的方式&#xff0c;但是如果思维是不清晰的&#xff0c;那么就算懂得再多的编码的奇技淫巧也是没有什么帮助的。相反&#xff0c;如果有一个清晰的…

Ubuntu20.04 Clion/Pycharm/IDEA 输入中文+光标跟随解决方案

ibus输入法&#xff08;弃用&#xff09; 之前一直用的搜狗输入法&#xff0c;但是搜狗输入法无法在Jetbrains全家桶下使用&#xff0c;但是又需要输入中文&#xff0c;没有办法我只能下载了谷歌输入法&#xff0c;十分难用&#xff0c;但是也没有其他办法&#xff0c;经常到网…

leetcode11.盛最多水的容器

题目描述 题目分析 看到题目后第一个想法当然是O(n2)O(n^2)O(n2)的&#xff0c;但是数据范围是3e4&#xff0c;应该会超时&#xff0c;而且这种数据范围也不是让暴力求解的 。 相当于求解∑i<jmax((j−i)∗min(a[i],a[j]))\sum_{i<j}{max((j-i)*min(a[i],a[j]))}∑i<…

每日一题:leetcode190.颠倒二进制位

题目描述 题目分析 题目本身很简单&#xff0c;没觉得有什么技巧可以再进行优化了&#xff0c;觉得位运算是无法打乱相对顺序的&#xff0c;而这里需要进行镜像颠倒的操作。因此就踏实地写了一个循环。 在使用位运算得到每一位的时候&#xff0c;我吸取了经验&#xff0c;用一…

结构屈曲分析

结构屈曲分析主要用于判定结构受载后是否有失稳风险&#xff0c;作为工程应用&#xff0c;一般分为线性屈曲分析和非线性屈曲分析。 线性屈曲分析需要具备较多的前提条件&#xff0c;如载荷无偏心、材料无缺陷等&#xff0c;在实际工程应用中结构制作过程和加载方式很难达到线性…

每日一题:leetcode74.搜索二维矩阵

题目描述 题目分析 感觉这是一个放错标签的简单题。题目非常简单&#xff0c;思路应该很明确是二分&#xff0c;我很快写了一个&#xff08;虽然不小心把!打成调试了一会&#xff09;。 class Solution { public:bool searchMatrix(vector<vector<int>>& mat…

每日一题:leetcode90.子集贰

题目描述 题目分析 感觉这道题让自己对枚举排列有了一个更好的认识&#xff0c;感觉自己的这种思路不错。 假设没有重复元素&#xff08;退化成78.子集&#xff09;&#xff0c;我们应该怎么做&#xff1f;初始的时候幂集中只有一个空集&#xff0c;然后对每个元素&#xff0…

每日一题:leetcode1006.笨阶乘

题目描述 题目分析 因为顺序一定且没有括号&#xff0c;所以逻辑很简单。我们要顺序处理的矛盾在于&#xff0c;减号后面会再出现乘法和除法&#xff0c;我们不妨将对乘法和除法用一个临时值进行计算&#xff0c;计算结束后再合并到值里面&#xff0c;一般来讲乘法和除法的处理…

每日一题:leetcode80.删除有序数组中的重复元素贰

题目描述 题目分析 又是一道贴错标签的简单题&#xff0c;很明显的双指针&#xff0c;我的做法是用两个变量保存是否需要记录&#xff0c;官方题解的做法是直接判断&#xff0c;人家的高明一些 class Solution { public:int removeDuplicates(vector<int>& nums) {…

每日一题:leetcode81.搜索旋转排序数组Ⅱ

题目描述 题目分析 不含重复元素的题解&#xff08;leetcode33&#xff09; 这道题也是我们算法课的一道编程题&#xff0c;写完以后发现当时的思路和现在没有什么变化&#xff0c;果然是自己啊。我的想法是先判断区间整体是升序的还是旋转的&#xff0c;如果是升序的就按照正…

C++ JSON库:JSON for Morden C++

绪论 最近因为项目的需要&#xff0c;需要对JSON进行一定的数据处理&#xff0c;因为想要用C进行编码&#xff0c;便对C的JSON库进行的调研&#xff0c;发现这个库比较好用&#xff1a;JSON for Morder C。 使用指南 想要使用这个json库&#xff0c;只需要在源文件中包含jso…

Linux信号实现精确到微秒的sleep函数:通过sigsuspend函数解决时序竞态问题

原理就是先使用定时器定时&#xff0c;然后再使用pause函数或者sigsuspend函数主动阻塞挂起&#xff0c;最终恢复现场。 如果使用pause函数的话&#xff0c;优点是使用简单&#xff0c;缺点是有可能产生时序竞态&#xff0c;导致进程一直阻塞下去&#xff1a;在定时和挂起之间…

Linux创建多个子进程并通过捕获SIGCHLD信号进行非阻塞回收

我们通过fork函数创建多个子进程&#xff0c;并通过exec函数族在子进程中进行其他的工作&#xff0c;但是为了避免僵尸进程&#xff0c;我们要对子进程进行回收。常用的回收方式是wait或者waitpid进行阻塞回收&#xff0c;因为如果非阻塞回收很难把握时机&#xff0c;而阻塞回收…

Linux创建守护进程

守护进程&#xff08;Daemon&#xff09;是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务&#xff0c;不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过…