闭关多日,整理一份C++中那些重要又容易忽视的细节

在这里插入图片描述

@[toc]


基础篇

喜欢用内联函数吗?

内联函数,都知道是什么嘛,就不多解释了,用这个开头,因为它够简单,又有足够的争议性。
有的人喜欢用内联函数,有的人不喜欢用,我嘛,无所谓,什么时候想起来就什么时候用,或者在代码审计的时候会去调整一部分函数为内联函数。

内联函数是C++为了提高程序运行速度所做的一项改进,让我们深入到函数内部,了解一下内联函数和常规函数的区别。

在C当中,是没有inline这个关键字的,C要使用类似的功能,就需要去写宏函数了,但是又不好写,并不是谁都能驾驭的了的。

C++在编译的时候,会将每个函数编译成一条条机器语言指令,在执行常规函数时,程序将会跳转到相应的地址,将参数复制到堆栈,跳到标记函数起点的内存单元,执行函数代码,并在函数结束时返回。

这反复横跳并记录过程的过程,是要有一定的开销的。

内联函数则提供了另一种可能,对于内联函数,编译器在编译的时候直接在调用处将函数展开,嵌入到调用函数中,所以无需反复横跳,减少了时间的开销,但是,增加了空间的开销。

应有选择的使用内联函数,因为它节省下来的时间确实是少得可怜,如果说执行函数代码的时间比函数调用机制的时间长,那用内联函数就没什么意思。

所以才会产生这么一个说法:在某个短小精悍的函数被多次调用时,才考虑将其转为内联函数,苍蝇肉,实在小,不过聚沙成塔嘛。


头文件与名空间,好用吧!

名空间的支持是一项C++特性,旨在让我们比阿尼写大型程序以及将多个厂商的现有代码组合起来的程序时更容易。

注意:假设名空间和声明区域定义了相同的名称,试图使用声明将名空间的名称导入该声明区域,则两个名称会发生冲突,从而出错。如果使用using编译指令将该名空间的名称导入该声明区域,则局部版本将隐藏名空间版本。

一般说来,using声明(要用什么就声明什么)比使用using编译指令(using namespace XXX)更安全,这是由于它只导入指定的名称,如果该名称与局部名称发生冲突,那你还导入它干嘛?

没用过,下次可以试试自己写一个名空间,如果是没有名字的名空间,那么只能在包含那个名空间的文件里面使用该名空间内部的内容,类似于,静态变量、函数的集合。


引用

首先,&不是地址运算符,而是类型标识符的一部分,就像声明中的char*是指向char的指针一样,int&是指向int的引用。

示例:

int a;
int &b = a;

上述引用声明允许将a和b互换,因为它们指向相同的值和内存单元。

不过呢,必须在声明引用变量的时候进行初始化:

int a;
int &b;
b = a;	//这样是不行的

返回引用的高效性

传统的返回机制是这样的:
1、获取返回值
2、将返回值复制到一个临时位置
3、调用函数从临时位置获取这个值

返回引用的返回机制是这样的:
1、获取返回值
2、直接将返回值拷贝给调用函数

如果返回值不大,那就不大,如果返回值是一个结构这种比较大的东西,那就比较麻烦了,能明白我意思不?

返回引用时,应避免返回函数终止时不再存在的内存单元引用(在指针里说过同样的话)。
为避免这种问题,最简单的方法就是:返回一个作为参数传递给函数的引用。


何时使用引用参数?

想用的时候呗。

使用引用参数这种“大招”的主要动机有:
1、程序员能够修改调用函数中的数据对象
2、可以提高程序的运行速度。

那么,==什么时候该使用指针,什么时候该使用引用,什么时候该使用按值传递呢==?

对象数据很小,按值传递即可。
对象是数组,指针。这是唯一的选择,并将指针声明为指向const的指针。
数据对象是较大的结构,使用const指针或const引用,提高程序效率。
数据对象是类对象,使用const引用。类设计的语义常常要求使用引用,因此,在传递类对象参数的标准方式是按引用传递。

对于修改调用函数中数据的函数:
	如果数据对象是内置数据类型,使用指针。
	如果对象是数组,只能使用指针。
	如果对象是结构,使用指针或引用都可以。
	如果对象是类对象,使用引用。

控制对成员的访问,是公有?是私有?

对新手来说,这个点估计是经常被忽略的吧。

数据项通常放在私有部分,组成类接口的成员函数放在公有部分。

为什么呢?这是C++的封装性。不然要类干什么?结构体不能用吗?

在后面讲设计模式的时候还会再细讲这一部分。


==插点题外话==
昨天我们老师给我们讲了意味深长的一段话。

现在你们年轻人不是很喜欢讲“内卷”嘛,然后用什么去对抗内卷,“躺平”嘛。

“用友”听说过吗?低代码听说过吗?
未来,这些前篇一律的基本代码,已经并不局限与本科生,专科生也可以做,甚至高中生都可以做。而某些本科生,还高人一等的姿态。

其实他讲低代码的时候,我想起来了QT的UI,只要你会拖控件,就可以做出界面来,代码可以后台自动生成。

而现在又有多少人,是面向百度编程的。

未来是低代码的时代,只会写低代码的人只会越来越卷,直到找不到工作。
所以不要把过多的时间花在那些低意义的学习上。

本科阶段,真正应该花时间去研究的,是算法,是操作系统,是数据库,是网络编程,是计网,是英语,等等这些东西。

不要以为你们是大数据专业的,真正有大数据的公司,会把数据给你吗?

这才是我心目中真正==人间清醒==的老师。

写给目前困惑的朋友,这篇的内容可能一周后你就不记得了,但是希望这段话对你有帮助吧。


运算符重载

C++允许将运算符重载扩展到用户定义的类型,重载运算符可以使代码看起来更自然。

要重载运算符,需要使用被称为运算符函数的特殊函数形式:

operator(argument-list)

下面的实例使用成员函数演示了运算符重载的概念:

#include <iostream>
using namespace std;
 
class Box
{
   public:
      // 重载 + 运算符,用于把两个 Box 对象相加
      Box operator+(const Box& b)
      {
         Box box;
         box.length = this->length + b.length;
         box.breadth = this->breadth + b.breadth;
         box.height = this->height + b.height;
         return box;
      }
   private:
      double length;      // 长度
      double breadth;     // 宽度
      double height;      // 高度
};

可重载运算符:

双目算术运算符 + (加),-(减),*(乘),/(除),% (取模)
关系运算符 ==(等于),!= (不等于),< (小于),> (大于),<=(小于等于),>=(大于等于)
逻辑运算符 ||(逻辑或),&&(逻辑与),!(逻辑非)
单目运算符 + (正),-(负),*(指针),&(取地址)
自增自减运算符 ++(自增),–(自减)
位运算符 | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移)
赋值运算符 =, +=, -=, *=, /= , % = , &=,
空间申请与释放 new, delete, new[ ] , delete[]
其他运算符 ()(函数调用),->(成员访问),,(逗号),[](下标)

面试题:C++类自动提供的成员函数

默认构造函数:如果没有定义构造函数
默认析构函数:如果没有定义
复制构造函数:、、、、
赋值运算符:、、、、
地址运算符:、、、、

当时面试的时候突然碰到这个问题,有感而发。


虚基类为什么需要虚析构函数?

直接来个示例看一下吧:

class test{ 
public:
virtual ~test() = 0; // 声明一个纯虚析构函数
};

==防止内存泄露==,定义一个基类的指针p,在delete p时,如果基类的析构函数是虚函数,这时==只会看p所赋值的对象==,如果p赋值的对象是派生类的对象,就会调用派生类的析构函数(毫无疑问,在这之前也会先调用基类的构造函数,在调用派生类的构造函数,然后调用派生类的析构函数,基类的析构函数,所谓先构造的后释放);如果p赋值的对象是基类的对象,就会调用基类的析构函数,这样就不会造成内存泄露。

如果基类的析构函数不是虚函数,在delete p时,调用析构函数时,==只会看指针的数据类型==,而不会去看赋值的对象,这样就会造成内存泄露。


虚函数的工作原理

通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存一个指向函数地址数组的指针。
这种数组称为虚函数表(virtual function table, vtbl)。
虚函数表中存储了为对象进行声明的虚函数的地址。

例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数也将被添加到vtbl中。注意,无论类中包含的虚函数是1个还是10个,都只需要在对象中添加1个地址成员,只是表的大小不同而已。

调用虚函数时,程序将查看存储在对象中的vtbl地址,然后转向相应的函数地址表。如果使用类声明中定义的第一个虚函数,则程序将使用数组中中的第一个函数地址,并执行具有该地址的函数。如果使用类声明中定义的第三个函数,程序将使用地址为数组中第三个元素的函数。

简而言之,使用虚函数时,在内存和执行速度方面有一定的成本,包括:

1)每个对象都将增大,增大量为存储函数地址表(数组)的空间。
2)对每个类,编译器都创建一个虚函数地址表(数组)。
3)每个函数调用都需要执行一部额外的操作,即到表中查找地址。

虽然非虚函数的效率比虚函数稍高,但不具备动态联编(Dynimac Blinding)的功能。


友元

以前看到这个模块儿都是直接划走,根本没兴趣。
但是,这几天尝试着了解了一下友元(主要是有几个大佬反复的跟我说过,友元,要用),我发现,学会友元,能让我对C++的认识更进一步。所以我来了。

了解一下友元函数吧

友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,其格式如下:

friend 类型 函数名(形式参数);

友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数。 一个函数可以是多个类的友元函数,只需要在各个类中分别声明。 友元函数的调用与一般函数的调用方式和原理一致。

友元函数虽然不是类成员却能够访问类的所有成员的函数。类授予它的友元特别的访问权。通常同一个开发者会出于技术和非技术的原因,控制类的友元和成员函数(否则当你想更新你的类时,还要征得其它部分的拥有者的同意)。

来个使用示例看一下:

    #include "iostream"
    using namespace std;
    class Point
    {
        int aa;
    public:
        friend void bb(Point cc);
        Point()
        {
           aa=88;
        }
    };
    void bb(Point cc)
    {
      int d=cc.aa; //通过对象的引用可以直接访问
      cout<<"这是友元函数通过对象的引用直接访问私有变量的例子!"<<endl;
      cout<<d<<endl;
    }
    /*
    void dd(Point cc)
    {
       int d=cc.aa; //不可以直接访问
        cout<<d<<endl;
    }
    */
    int main()
    {
        Point p;
        bb(p);
        return 0;
    }

友元函数是否破坏了类的封装性

至于它是否破坏了类的封装性,这个不同的人有不同的说法啦,认为它没有破坏封装性的人觉得只有类声明可以控制哪些函数可以访问内部数据。
仁者见仁,智者见智吧。

我看到一段比较好的解答:
我们已知道类具有封装和信息隐藏的特性。只有类的成员函数才能访问类的私有成员,程序中的其他函数是无法访问私有成员的。非成员函数可以访问类中的公有成员,但是如果将数据成员都定义为公有的,这又破坏了隐藏的特性。另外,应该看到在某些情况下,特别是在对某些成员函数多次调用时,由于参数传递,类型检查和安全性检查等都需要时间开销,而影响程序的运行效率。

为了解决上述问题,提出一种使用友元的方案。友元是一种定义在类外部的普通函数,但它需要在类体内进行说明,为了与该类的成员函数加以区别,在说明时前面加以关键字friend。友元不是成员函数,但是它可以访问类中的私有成员。友元的作用在于提高程序的运行效率,但是,它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。


什么时候使用友元函数:

1)运算符重载的某些场合需要使用友元。

2)两个类要共享数据的时候

略显疲惫呀

(完)