【编程语言学习】C++(1) 基础语法
本文最后更新于 28 天前,其中的信息可能已经有所发展或是发生改变。

本文档记录所有内容均以C++11标准为基础

本文档侧重于C++中与C语言重叠或相似的知识,并进行一定扩展

参考文档

《C++ Primer 第五版 中文版》 –电子工业出版社

菜鸟教程(runoob.com)

1. 引用

引用reference并不是对象而是为对象起了另外一个名字,引用类型引用另外一种类型。通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名

一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。

1.1 引用的定义

允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头:

int i = 1024, i2 = 2048;    //i和i2都是int
int sr = i, r2 = i2;        //r是一个引用,与i绑定在一起,r2是int
int i3 = 1024, &r1 = i3;    //i3是int,r1是一个引用,与i3绑定在一起
int &r3 = i3, &r4 = i2;     //r3和r4都是引用

引用只能绑定在一个对象上,而不能是一个值或者计算式的结果

int &refVal4 = 10;       //错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 =dval;     //错误:此处引用类型的初始值必须是int型对象

1.2 引用和指针的区别

指针和引用都是地址的概念,指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。

  • 程序为指针变量分配内存区域,而不为引用分配内存区域。
  • 指针使用时要在前加 * ,引用可以直接使用。
  • 引用在定义时就被初始化,之后无法改变;指针可以发生改变。 即引用的对象不能改变,指针的对象可以改变。
  • 没有空引用,但有空指针。这使得使用引用的代码效率比使用指针的更高。因为在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。
  • 对引用使用sizeof得到的是变量的大小,对指针使用“sizeof”得到的是变量的地址的大小。
  • 理论上指针的级数没有限制,但引用只有一级。即不存在引用的引用,但可以有指针的指针 int **p //合法 int &&p //非法
  • ++引用与++指针的效果不一样。
    例如就++操作而言,对引用的操作直接反应到所指向的对象,而不是改变指向;而对指针的操作,会使指针指向下一个对象,而不是改变所指对象的内容。

2. const限定符

尽量以constenuminline替换#define –《Effective C++中文版 第三版》条款02

有时我们希望定义这样一种变量,它的值不能被改变。例如,用一个变量来表示缓冲区的大小。使用变量的好处是当我们觉得缓冲区大小不再合适时,很容易对其进行调整。另一方面,也应随时警惕防止程序一不小心改变了这个值。为了满足这一要求,可以用关键字const对变量的类型加以限定

2.1 const对象的特征

const修饰的变量是一个常量,任何对其赋值的操作都将引发错误

const int buf = 125;
buf = 1;              //错误:试图向const对象写值

const对象必须初始化,初始值可以是任意复杂的表达式:

int get_size()
{
    return 0;
}

const int i = get_size();     //正确:运行时初始化
const int j = 42;             //正确:编译时初始化
const int k;                  //错误:k是一个未经初始化的常量

const对象可以在初始化时被使用

int i = 42;
const int ci = i;    //正确:i的值被拷贝给了ci
int j= ci;           //正确:ci的值被拷贝给了j

默认状态下const对象仅在文件内有效

  • 在链接期间,c++const具有内部链接性,cconst默认外部链接性
  • 编译器将在编译过程中把用到该变量的地方都替换成对应的值,译器必须知道变量的初始值。如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义
  • 如果需要在文件间共享这个对象,那么不管声明还是定义都添加extern关键字,这样只需定义一次就可以了

2.2 const的引用

可以把引用绑定到const对象上,就像绑定到其他对象上一样,我们称之为对常量的引用。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:

const int ci = 1024;
const int &r1 = ci;  //正确:引用及其对应的对象都是常量
r1 = 42;             //错误:r1是对常量的引用
int &r2 = ci;        //错误:试图让一个非常量引用指向一个常量对象,不能直接或是通过引用修改常量对象的值

3. auto类型说明符

C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型,和某种特定类型的说明符不同,auto让编译器通过初始值来推算变量的类型。

3.1 注意事项

auto定义的变量必须有初始值

使用auto也能在一条语句中声明多个变量,因为一条声明语句中只能有一个基本类型,所以该语句中所有变量的初始数据类型必须一样

auto i = 0, *p = &i;        //正确:i是整数、p是整型指针
auto sz = 0, pi = 3.14;        //错误:sz和pi的类型不一样

编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当的改变结果类型使其更符合初始化规则

4. 范围for循环

如果想要对string对象中的每个字符做点什么操作,可以使用C++11新标准提供的范围for语句,这种语句遍历给定序列中的每个元素

4.1 原型及示例

语法形式如下,expression部分是一个对象,用于表示一个序列。declaration部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,declaration部分的变量会被初始化为expression部分的下一个元素值。

for (declaration:expression)
    statement

一个string对象表示一个字符的序列,因此string对象可以作为范围for语句中的expression部分。

std::vector<int> numbers = {1, 2, 3, 4, 5};
for (int number : numbers) {
    std::cout << number << " ";
}
std::cout << std::endl;
//输出:1 2 3 4 5

4.2 改变字符串中的字符

如果想要改变string对象中字符的值,必须把循环变量定义成引用类型。当使用引用作为循环控制变量时,这个变量实际上被依次绑定到了序列的每个元素上。使用这个引用,我们就能改变它绑定的字符。

std::string s("Hello World!!!");     //转换成大写形式。
for(auto &c : s) {                   //对于s中的每个字符(注意:c是引用)
    c = toupper(c);                  //c是一个引用,因此赋值语句将改变s中字符的值
}
std::cout << s <<std::endl;
//输出:HELLO WORLD!!!

5. C字符串和C++字符串

对于字符串的处理两种语言的处理有很大的不同,C语言中通常使用char*类型的裸指针操作字符串,在字符串结尾部分放上\0标志字符串结束。C++提供了多种机制便于处理字符串,同时又避免可能会出现的内存问题。

此文仅简述C++对于字符串处理所提供的部分机制和语法糖,不深入讨论底层实现和C语言的字符串处理机制

5.1 标准库类型string

C++标准库提供了string类型表示可变长的字符序列,使用string类型必须首先包含string头文件。作为标准库的一部分,string定义在命名空间std中。在使用string类型前需保证代码中已正确包含了头文件并声明了命名空间。

#include <string>
using std::string;

5.1.1 定义和初始化string对象

对于string对象的初始化有多种不同的方式,下面是几个例子

string s1;                //默认初始化,s1是一个空字符串
string s2(s1);            //s2是s1的副本
string s2 = sl;            //等价于s2(s1),s2是s1的副本
string s3("hiya");        //s3是字面值“hiya"的副本除了最后面的那个空字符外
string s3 = "hiya";        //等价于string s3 ="hiya";,s3是该宇符串字面值的副本
string s4(10,'c');        //把s4初始化为由连续n个字符c组成的字符串

如果使用等号=初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化。

当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式:

string s5 = "hiya";        //拷贝初始化
string s6("hiya");        //直接初始化
string s7(10,'c');        //直接初始化,s7的内容是cccccccccc

5.1.2 string对象的常见操作

  • 比较string对象 string类定义了几种用于比较字符串的运算符。这些比较运算符逐一比较string
  • 相等性运算符==!=分别检验两个string对象相等或不相等,string对象相等意味着它们的长度相同而且所包含的字符也全都相同。
  • 关系运算符<<=>>=分别检验一个string对象是否小于、小于等于、大于、大于等于另外一个string对象。上述这些运算符都依照(大小写敏感的)字典顺序
  • 两个string对象相加 对两个string对象使用加法运算符+得到一个新的string对象,其内容是把左侧的运算对象与右侧的运算对象串接而成。
  string s1 = "hello,",s2 = "world\n";
  string s3 = s1 +s2;        //s3的内容是hello,world\n
  s1 += s2;                //等价于s1 =s1 +s2
  • 字面值和string对象相加 当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符+的两侧的运算对象至少有一个是string。此时运算的结果会自动的类型转换成string
  string s4 =s1 +",";            //正确:把一个string对象和一个字面值相加
  string s5="hello"+",";      //错误:两个运算对象都不是string
  string s6 =s1+","+"world";    //正确:每个加法运算符都有一个运算对象是string
  string s7=“hello”+","+s2;    //错误:不能把宇面值直接相加

5.1.3 注意事项

  • string可以使用下标运算符输出其中的任意一个字符,这与数组相同
  • 为了保持与C语言兼容,C++中的字符串字面值并不是标准库类型string的对象,字符串字面值与string是不同的类型

5.2 标准库类型vector

可以使用string类型来构建vector对象

vector<string>v5{"hi"};        //列表初始化:v5有一个元素
vector<string>v6("hi");        //错误:不能使用字符串字面值构建vector对象
vector<string>v7{10};        //v7有10个默认初始化的元素
vector<string>v8{10,"hi"};    //v8有10个值为"hi"的元素

6. 命名空间

假设这样一种情况,当一个班上有两个名叫 Zara 的学生时,为了明确区分它们,我们在使用名字之外,不得不使用一些额外的信息,比如他们的家庭住址,或者他们父母的名字等等。

同样的情况也出现在 C++ 应用程序中。例如,您可能会写一个名为 xyz() 的函数,在另一个可用的库中也存在一个相同的函数 xyz()。这样,编译器就无法判断您所使用的是哪一个 xyz() 函数。

因此,引入了命名空间这个概念,专门用于解决上面的问题,它可作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围

例如std::cin表示从标准输入中读取内容,此处使用作用域操作符::的含义是:编译器应该从操作符左侧名字的作用域中寻找右侧的这个名字,因此std::out的意思是是要使用命名空间std中的名字cin

6.1 using声明

有了using声明就无需专门的前缀也可以使用所需的名字,using声明具有如下的形式

//using声明的形式
using namespace::name;

#include<iostream>
//using声明,当我们使用名字cin的时候,从命名空间std中获取它
using std:cin;

int main()
{
    int i;
    cin >> i;            //正确:cin和std::cin含义相同
    cout << i;            //错误:没有对应的using声明,必须使用完整的名字
    std::cout << i;        //正确:显示的从std中使用cout
    return 0;
}

7. 结构体

7.1 C与C++中struct的区别

C语言和C++中,都可以使用结构体struct来定义自定义的数据类型。两种编程语言有许多不同的地方

  1. C语言中,结构体主要用来组织不同类型的数据成员,而在C++中,结构体除了可以拥有数据成员外,还可以拥有成员函数。
  2. C++中可以用于对于成员的访问权限控制,C的结构体对内部成员变量的访问权限只能是public,而C++允许publicprivateprotected三种
  3. C的结构体是不可以继承的,C++的结构体是允许从其他结构体或者类继承的

C语言中,定义一个结构体可以这样写:

struct Person {
    char name[50];
    int age;
};

而在C++中,除了上述写法外,还可以加上访问权限控制和成员函数:

struct Person {
private:
    char name[50];
    int age;

public:
    void setName(const char* newName) {
        strcpy(name, newName);
    }

    void setAge(int newAge) {
        age = newAge;
    }

    void displayInfo() {
        cout << "Name: " << name << endl;
        cout << "Age: " << age << endl;
    }
};

7.2 成员函数的实现

C++中如果一个结构体中的一个成员函数只是函数声明而不包含函数实现,就需要在外部实现

// 结构体声明
struct MyStruct {
    void myFunction(); // 声明但未实现的函数
};

// 结构体外部定义函数实现
void MyStruct::myFunction() {
    std::cout << "Function is implemented" << std::endl;
}

int main() {
    MyStruct s;
    s.myFunction();  // 调用函数
    return 0;
}

C语言中如果想在结构体中添加函数,则只能添加函数指针。不能在结构体中声明函数或定义函数

//声明函数指针类型
typedef void (*myFunction)(void);

//C语言中结构体成员想要是函数,只能使用函数指针
struct myStruct
{
    myFunction func;
};

//函数实现
void printDemo()
{
    printf("Hello World\n");
}

int main()
{
    struct myStruct demo;

    demo.func = printDemo;
    demo.func();

    return 0;
}

8.类

类的基本思想是数据抽象和封装。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。

类要想实现数据抽象和封装,需要首先定义一个抽象数据类型。在抽象数据类型中,由类的设计者负责考虑类的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么,而无须了解类型的工作细节。

类的主体是包含在一对花括号中,主体包含类的成员变量和成员函数。定义一个类,本质上是定义一个数据类型的蓝图,它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作

8.1 类作用域和成员函数

类本身就是一个作用域,类的成员函数的定义嵌套在类的作用域之内,编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体(如果有的话)。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。

因此,isbn中用到的名字bookNo其实就是定义在Sales_data内的数据成员。值得注意的是,即使bookNo定义在isbn之后,isbn也还是能够使用bookNo

struct Sales_data (
    //新成员:关于Sales_data对象的操作
    std::string isbn() {return bookNo;}
    Sales_data6 combine (const Sales_datak);
    double avg_price();
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

//Sales_data的非成员接口函数
Sales_data add(const Sales_datas, const Sales_datab);
std::ostream aprint(std::ostreams, const Sales_datab);
std::istream sread(std::istreams ,Sales_dataf);

8.2 类访问修饰符

数据封装是面向对象编程的一个重要特点,它防止函数直接访问类类型的内部成员。类成员的访问限制是通过在类主体内部对各个区域标记 publicprivateprotected 来指定的。关键字 publicprivateprotected 称为访问修饰符

  • 公有public成员在程序中类的外部是可访问的。您可以不使用任何成员函数来设置和获取公有变量的值
  • 私有private成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。默认情况下,类的所有成员都是私有的
  • 受保护protected成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的

8.3 类的继承

面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类

一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:

class derived-class: access-specifier base-class

其中,访问修饰符 access-specifierpublicprotectedprivate 其中的一个,base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private

例如:哺乳动物是动物,狗是哺乳动物,因此,狗是动物

对应C++代码为

// 基类
class Animal {
    // eat() 函数
    // sleep() 函数
};
//派生类
class Dog : public Animal {
    // bark() 函数
};

8.3.1 访问控制和继承

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。

我们可以根据访问权限总结出不同的访问类型,如下所示:

访问publicprotectedprivate
同一个类yesyesyes
派生类yesyesno
外部的类yesnono

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

8.3.2 派生类的成员权限

对于不同的派生类继承方式,相应地改变了基类成员的访问属性

  • public 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private
  • protected 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private
  • private 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private 整理成表格如下 继承方式\基类成员属性 基类 public 成员 基类protected 成员 基类private 成员 public 继承 public protected private protected 继承 protected protected private private 继承 private private private

但无论哪种继承方式,下面两点都没有改变:

  • private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;
  • protected 成员可以被派生类访问。

8.4 多继承

多继承即一个子类可以有多个父类,它继承了多个父类的特性

C++ 类可以从多个类继承成员,语法如下:

class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};

访问修饰符与单继承相同,是 publicprotectedprivate 其中的一个

#include <iostream>

using namespace std;

// 基类 Shape
class Shape 
{
   public:
      void setWidth(int w)
      {
         width = w;
      }
      void setHeight(int h)
      {
         height = h;
      }
   protected:
      int width;
      int height;
};

// 基类 PaintCost
class PaintCost 
{
   public:
      int getCost(int area)
      {
         return area * 70;
      }
};

// 派生类
class Rectangle: public Shape, public PaintCost
{
   public:
      int getArea()
      { 
         return (width * height); 
      }
};

int main(void)
{
   Rectangle Rect;
   int area;

   Rect.setWidth(5);
   Rect.setHeight(7);

   area = Rect.getArea();

   // 输出对象的面积
   cout << "Total area: " << Rect.getArea() << endl;

   // 输出总花费
   cout << "Total paint cost: $" << Rect.getCost(area) << endl;

   return 0;
}

//程序输出
Total area: 35
Total paint cost: $2450

8.5 访问类成员

C++中类成员的访问和结构体成员的访问在语法上几乎一致,在此不作赘述

9. 类的构造函数与析构函数

C++中用构造函数和析构函数来初始化和清理对象,这两个函数将会被编译器自动调用。对象的初始化和清理是非常重要的,如果我们不提供构造函数与析构函数,编译器会自动提供两个函数的空实现。

构造函数:主要作用于创建函数时对对象成员的属性赋值。
析构函数:主要作用于在对象销毁前,执行一些清理工作(如释放new开辟在堆区的空间)。

9.1 构造函数

构造函数语法:类名(){}

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
  4. 一个类可以有多个构造函数 ,可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载

无参数构造函数

例如:

class Line
{
   public:
      Line();  // 这是构造函数

   private:
      double length;
};

// 成员函数定义,包括构造函数
Line::Line(void)
{
    cout << "Object is being created" << endl;
}

// 程序的主函数
int main( )
{
   Line line;
   return 0;
}
//程序输出:
Object is being created

有参数构造函数

例如:

class Line
{
   public:
      Line(double len);  // 这是构造函数

   private:
      double length;
};

// 成员函数定义,包括构造函数
Line::Line( double len)
{
    cout << "Object is being created, length = " << len << endl;
    length = len;
}

// 程序的主函数
int main( )
{
   Line line(10.0);

   return 0;
}
//程序输出:
Object is being created, length = 10

9.2 析构函数

析构函数语法: ~类名(){}

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次

例如:

class Line
{
   public:
      Line();   // 这是构造函数声明
      ~Line();  // 这是析构函数声明 
   private:
      double length;
};

// 成员函数定义,包括构造函数
Line::Line(void)
{
    cout << "Object is being created" << endl;
}
Line::~Line(void)
{
    cout << "Object is being deleted" << endl;
}

// 程序的主函数
int main( )
{
   Line line;
   return 0;
}
//程序输出:
Object is being created
Object is being deleted

其余特点:

  • 构造函数和析构函数是一种特殊的公有成员函数,每一个类都有一个默认的构造函数和析构函数
  • 构造函数在类定义时由系统自动调用,析构函数在类被销毁时由系统自动调用
  • 构造函数的名称和类名相同,一个类可以有多个构造函数只能有一个析构函数。不同的构造函数之间通过参数个数和参数类型来区分
  • 我们可以在构造函数中给类分配资源,在类的析构函数中释放对应的资源
  • 如果程序员没有提供构造和析构,系统会默认提供,空实现
  • 构造函数 和 析构函数,必须定义在public里面,才可以调用

10. 友元

如果将类的封装比喻成一堵墙的话,那么友元机制就像墙上了开了一个门,那些得到允许的类或函数允许通过这个门访问一般的类或者函数无法访问的私有属性和方法。友元机制使类的封装性得到消弱,所以使用时一定要慎重。

–《windows环境多线程编程原理与应用》

类的友元函数是定义在类外部,但有权访问类的所有私有private成员和保护protected成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。友元的声明需要使用friend关键字

10.1 全局函数作为友元

如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend,如下所示:

class Box
{
   double width;
public:
   friend void printWidth( Box box );
   void setWidth( double wid );
};

// 成员函数定义
void Box::setWidth( double wid )
{
    width = wid;
}

// 全局函数
void printWidth( Box box )
{
   /* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
   cout << "Width of box : " << box.width <<endl;
}

int main()
{
   Box box;

   box.setWidth(10.0);
   printWidth( box );
   return 0;
}

10.2 类作为友元

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)

声明类 ClassTwo 的所有成员函数作为类 ClassOne 的友元,需要在类 ClassOne 的定义中放置如下声明:

friend class ClassTwo;

可以将整个类作为友元,这样类中的所有成员函数都可以访问其私有和保护成员

class goodfriends
{
public:
    goodfriends();//构造函数
    void visit();//访问函数
    void visit1();//访问函数
};
class Building
{
    friend class goodfriends;//goodfriends是这个类的朋友,可以访问这个类里面的参数和函数
public:
    Building();
private:
    string M_bedroom;
};

10.3 成员函数做友元

将类中的某个成员函数作为友元

class goodfriends
{
public:
    goodfriends();//构造函数
    void visit();//访问函数
};
class Building
{
   friend void goodfriends:: visit1();//访问函数
public:
    Building();
private:
    string M_bedroom;
};

使用友元类时注意:

  1. 友元关系不能被继承
  2. 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明
  3. 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

11. 重载

C++允许在同一作用域中的某个函数运算符指定多个定义,分别称为函数重载运算符重载

重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同

当您调用一个重载函数重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策

11.1 函数重载

在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。您不能仅通过返回类型的不同来重载函数

  • 函数的参数个数、参数类型、参数顺序不同三者中满足其中一个,就是函数重载了
  • 如果只有函数返回值不同,不是函数重载;返回值不同,参数也不同的时候,可以作为函数重载

例如:

class printData
{
   public:
      void print(int i) {
        cout << "整数为: " << i << endl;
      }

      void print(double  f) {
        cout << "浮点数为: " << f << endl;
      }

      void print(char c[]) {
        cout << "字符串为: " << c << endl;
      }
};

int main(void)
{
   printData pd;
   char c[] = "Hello C++";

   pd.print(5);// 输出整数
   pd.print(500.263);// 输出浮点数
   pd.print(c);// 输出字符串

   return 0;
}

11.2 运算符重载

运算符重载的本质是一个函数,是从函数一步步演绎而来;重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表

Box operator[运算符](const Box&);                        //运算符重载声明
Box operator[运算符](const Box&){[运算符重载运行内容]}      //运算符重载定义

例如

class Complex {
public:
    double real;
    double imag;

    Complex(double r, double i) {
        real = r;
        imag = i;
    }
    //表达式 a + b 中,a 是调用对象,因此重载运算符函数会被调用,而 b 则作为该重载运算符函数的参数传入,此处参数列表只有一个元素
    Complex operator+ (const Complex& other) {
        Complex result(0.0, 0.0);
        result.real = real + other.real;
        result.imag = imag + other.imag;
        return result;
    }

    void display() {
        std::cout << real << " + " << imag << "i" << std::endl;
    }
};

int main() {
    Complex c1(2.0, 3.0);
    Complex c2(1.0, 1.0);
    Complex c3 = c1 + c2;

    std::cout << "c1 = ";
    c1.display();

    std::cout << "c2 = ";
    c2.display();

    std::cout << "c3 = c1 + c2 = ";
    c3.display();

    return 0;
}

11.2.1 重载运算符参数数量

  • 重载单目运算符时,如果实现为成员函数,则一般需要 0 个参数,如果实现为非成员函数,则一般需要 1 个参数
  • 重载双目运算符时,如果实现为成员函数,则一般需要 1 个参数,如果实现为非成员函数,则一般需要 2 个参数
  • 小括号运算符,参数可以有任意多个。
  • 例外情况:后置自增 (减) 运算符,它们虽然是单目运算符。但是因为需要与前置自增 (减) 运算符区别,人为加了个 int 型参数

11.2.2 可重载和不可重载运算符

可重载的运算符列表:

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

不可重载的运算符列表:

成员访问运算符.
成员指针访问运算符., ->
域运算符::
长度运算符sizeof
条件运算符?:
预处理符号#
上一篇
下一篇