【C++初阶】—— 类和对象 (下)

简介: 【C++初阶】—— 类和对象 (下)

前言:类的6个默认成员函数,我们了解三个,讲完剩下的成员函数,其实类和对象的大致内容已经结束,最后我们在了解一些C++类和对象的剩下的的细节,我们就正式结束类和对象

如果你还对前面三个默认成员函数不太了解,建议先阅读这篇博客

类的成员函数


1. 运算符重载

运算符重载

在一个自定义变量里,如果我们想实现对它的加减乘除,是无法直接使用的,因此C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数

关键字operator 后面接需要重载的运算符符号

函数原型: 返回值类型 operator操作符(参数列表)

举个例子:

// 重载 ==
bool operator==(const Date& d)
{
  return _year = d._year;
  && _month = d._month;
  && _day = d._day;
}

注意:

  • 重载操作符必须有一个自定义类型参数
  • 运算符重载定义在类外时不能访问类中的私有成员,因此重载成成员函数
  • 作为类成员函数重载时,成员函数的第一个参数为隐藏的this

赋值运算符重载

1. 关于赋值运算符重载:

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回 *this

我们以下例子将使用日期类

例如:

class Date
{
public:
  Date()
  {}
  Date(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }

  void Print()
  {
    cout << _year << ' ' << _month << ' ' << _day << endl;
  }

  Date& operator=(const Date& d)
  {
    // 检查是否给直接赋值
    if (this != &d)
    {
      _year = d._year;
      _month = d._month;
      _day = d._day;
    }

    return *this;
  }
private:
  int _year;
  int _month;
  int _day;
};

int main()
{
  Date d1(2024, 5, 23);
  Date d2 = d1;
  // Date d2;
  // 实际上operator=的调用
  // d2.operator=(d1);
  d1.Print();
  d2.Print();
  return 0;
}

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

// 假设我们在类外面重载成全局函数
// 注意:在类外是没有 this 指针的
Date& operator=(Date& this, const Date& d)
{
  if (&this != &d)
  {
    this._year = d._year;
    this._month = d._month;
    this._day = d._day;
  }
  return this;
}

我们将写好的代码拿去运行一下,我们发现无法编译

其实,赋值运算符比较特殊如果不显式实现,编译器会生成一个默认的。如果在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了


3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝

这里我们要格外注意:

系统默认生成一个默认赋值运算符重载它和之前的拷贝构造一样,发生的是浅拷贝,内置类型成员变量可以直接使用,而自定义类型成员变量需要我们自己调用对应类的赋值运算符重载


前置++和后置++重载

关于前置++和后置++:

  • 前置++:返回+1之后的结果
  • 后置++:是先使用后+1,因此需要返回+1之前的旧值

格式:

  • 因为前置++和后置++符号一样,我们为了要想正确完成重载,C++规定,后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
// 前置++
Date& operator++()
{
  _day += 1;
  return *this
}
// 后置++
Date& operator++(int)
{
  Date temp(*this);
  _day += 1;
  return temp;
}
// 前置--
Date& operator--()
{
  _day -= 1;
  return *this
}
// 后置--
Date& operator--(int)
{
  Date temp(*this);
  _day -= 1;
  return temp;
}

最后补充一点,关于运算符重载,并不是所有的运算符都需要重载,而是要根据自定义的类需要重载哪些运算符!

注意以下运算符不能重载:

  • .*
  • ::
  • sizeof
  • ?:
  • .

讲到这里类和对象的大致内容已经结束,剩下两个成员函数,我们简单了解一下


2. 成员函数的补充

const成员

     将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

例如:

class Date
{
public:
  Date(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << "Print()" << endl;
    cout << "year:" << _year << endl;
    cout << "month:" << _month << endl;
    cout << "day:" << _day << endl << endl;
  }
  void Print() const
  {
    cout << "Print()const" << endl;
    cout << "year:" << _year << endl;
    cout << "month:" << _month << endl;
    cout << "day:" << _day << endl << endl;
  }
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
  // 编译器会优先调用符合的函数,如果没有则会根据权限来调用
  // 本质是:权限能缩小,但是不能放大
  // 及非const对象可以调用const成员函数
  // 非const成员函数内可以调用其它的const成员函数
  Date d1(2024,5,23);
  d1.Print();
  const Date d2(2024,5,23);
  d2.Print();
}

取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成!

Date* operator&()
{
  return this ;
}

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

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,我们能够修改别人获取的地址


3. 初始化列表

  • 在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值
  • 对象中有了一个初始值,因此构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值

初始化列表的概念

初始化列表: 以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式

Date(int year, int month, int day)
    : _year(year)
    , _month(month)
  {
    _day = day
  }
  // 函数体里面能够放数据
//Date(int year, int month, int day)
//{
//    _year = year;
//    _month = month;
//    _day = day;
//}

初始化列表的特征

使用初始化列表时注意:

  • 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  • 类中包含以下成员,必须放在初始化列表位置进行初始化:
  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时)

例如:

class A
{
public:
  A(int a)
    :_a(a)
  {}
private:
  int _a;
};

class B
{
public:
  B(int a, int ref)
    :_aobj(a)
    ,_ref(ref)
    ,_n(10)
  {}
private:
  A _aobj; // 没有默认构造函数
  int& _ref; // 引用
  const int _n; // const
};
int main()
{
  B bb(1,2);
  A aa(1);
}

特征:

1. 尽量使用初始化列表初始化

因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先经过初始化列表初始化

2. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

3. 当用户没有显示传参初始化时,编译器会用用户定义的缺省值

public:
  B(int a)
    :b(a) // b = 1;
  {}
private:
  int b = 1;

explicit关键字

构造函数不仅能构造和初始化对象,对于单个参数或除第一个参数无默认值,其余均有默认值的构造函数,还有隐式类型转换的作用,隐式类型转换是在编程中编译器自动进行的一种类型转换方式

class pxt
{
public:
  explicit pxt(int a = 0)
    :_a(a)
  {
    cout << "pxt(int a)" << endl;
  }
  ~pxt()
  {
    cout << "~pxt()" << endl;
  }
private:
  int _a;
};
int main()
{
  pxt a1 = 2024;
  // 用一个整形变量给自定义类型对象赋值
  // 编译器会用2024构造一个无名对象,最后用无名对象给a1对象进行赋值
  // 正常情景是能赋值的,但是explicit修饰构造函数后,会禁止构造函数的隐式转换
  return 0;
}

关键字explicit修饰构造函数,将会禁止构造函数的隐式转换


4. static成员

static成员的概念

概念:

  • 声明为static的类成员称为类的静态成员,
    static修饰的成员变量,称之为静态成员变量,
    static修饰的成员函数,称之为静态成员函数
  • 静态成员变量一定要在类外进行初始化
class pxt
{
public:
  void Print()
  {
    cout << _a << endl;
  }
private:
  // 在类中声明
  static int _a;
  //如果是静态成员函数,则没有this指针
};
// 在类外定义
int pxt:: _a = 100;

int main()
{
  pxt A;
  A.Print();
}

static成员的特征

特性:

  • 静态成员为所有类对象所共享,存放在静态区
  • 静态成员变量必须在类外定义,类中只是声明
  • 类静态成员可用 类名::静态成员 或者 对象.静态成员 来访问
  • 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  • 静态成员也是类的成员,受访问限定符的限制

5. 友元

友元: 提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元分为:

  • 友元函数
  • 友元类

友元函数

如果尝试去重载operator<<,我们发现没办法将operator<<重载成成员函数,因为函数的参数位置不一样,cout的输出流对象和隐含的this指针在抢占第一个参数的位置,重载operator>>同理

d << cout; -> d.operator<<(&d, cout); 不符合常规调用
因为成员函数第一个参数一定是隐藏的this,所以d必须放在<<的左侧

但是问题来了,如果我们写成全局函数,又无法使用私有的成员变量,这时友元的作用就凸显出来了!

友元函数: 可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字

例如:

class Date
{
  // 不声明友元,将无法调用私有成员
  friend ostream& operator<<(ostream& _out, const Date& d);
public:
  Date(int year, int month, int day)
    : _year(year)
    , _month(month)
    , _day(day)
  {}
  //void operator<< (ostream& _out)
  //{
  //  _out << _year << " " << _month << " " << _day;
  //}
private:
  int _year; // 年
  int _month; // 月
  int _day; // 日
};

ostream& operator<< (ostream& _out, const Date& d)
{
  _out << d._year << " " << d._month << " " << d._day;
  return _out;
}

int main()
{
  Date d(2024,5,23);
  cout << d << endl;
  // d << cout;
}

关于友元函数有以下几点:

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同

友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员

友元类的特征:

友元关系是单向的,不具有交换性

友元关系不能传递

如果C是B的友元, B是A的友元,则不能说明C时A的友元

友元关系不能继承,在继承位置再给大家详细介绍

关于友元关系的单向性我举个例子:

class A
{
  friend class B;
public:
  // ......
private:
  int _year;
  int _month;
  int _day;
};
class B
{
public:
  void test(int year, int month, int day)
  {
  // 直接访问A类私有的成员变量
  // 但是A 不能访问B 中私有的成员变量
  _d._year = year;
  _d._month = month;
  _d._day = day;
}
private:
  int good;
  A _d;
};

B能直接访问A类私有的成员变量,但是A 不能访问B 中私有的成员变量


讲到友元类,我们再来介绍一下一个跟友元类有很大关系的内部类

内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限

注意:内部类就是外部类的友元类,内部类能访问外部类中的所有成员,反之则不能!

class A
{
public:
  // ......
  A(int year = 2024, int month = 5, int day = 20)
    : _year(year)
    ,_month(month)
    ,_day(day)
  {}
  class B
  {
  public:
    void test(const A& _d)
    {
      cout << _d._year << " " << _d._month << " " << _d._day << endl;
    }
  };
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  A::B b;
  b.test(A());
}

内部类的特征

特性:

  • 内部类可以定义在外部类的所有成员
  • 内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
  • sizeof(外部类)=外部类,和内部类没有任何关系

6. 类的匿名对象

class pxt
{
public:
  pxt(int a = 0)
    :_a(a)
  {
    cout << "pxt(int a)" << endl;
  }
  ~pxt()
  {
    cout << "~pxt()" << endl;
  }
private:
  int _a;
};
int main()
{
  // 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
  // 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
  // 匿名对象
  pxt();
  // 隐式类型转换
  pxt a1 = 2024;
  return 0;
}

  • 生命周期只有一行,会自动调用析构函数
  • 匿名对象的特点不用取名字

因此当我们只是想使用类中的某一个函数时,我们能创建匿名对象!


7. 总结

类和对象的所有内容已经了解完毕,类和对象在整个C++上都有举足轻重的作用,大家千万不要忽视,而类和对象的重点在四个成员函数上,下节我将学习C++的内存管理

谢谢大家支持本篇到这里就结束了,祝大家天天开心!

目录
相关文章
|
13天前
|
设计模式 安全 编译器
【C++11】特殊类设计
【C++11】特殊类设计
33 10
|
18天前
|
C++
C++友元函数和友元类的使用
C++中的友元(friend)是一种机制,允许类或函数访问其他类的私有成员,以实现数据共享或特殊功能。友元分为两类:类友元和函数友元。类友元允许一个类访问另一个类的私有数据,而函数友元是非成员函数,可以直接访问类的私有成员。虽然提供了便利,但友元破坏了封装性,应谨慎使用。
46 9
|
13天前
|
存储 编译器 C语言
【C++基础 】类和对象(上)
【C++基础 】类和对象(上)
|
21天前
|
编译器 C++
【C++】string类的使用④(字符串操作String operations )
这篇博客探讨了C++ STL中`std::string`的几个关键操作,如`c_str()`和`data()`,它们分别返回指向字符串的const char*指针,前者保证以&#39;\0&#39;结尾,后者不保证。`get_allocator()`返回内存分配器,通常不直接使用。`copy()`函数用于将字符串部分复制到字符数组,不添加&#39;\0&#39;。`find()`和`rfind()`用于向前和向后搜索子串或字符。`npos`是string类中的一个常量,表示找不到匹配项时的返回值。博客通过实例展示了这些函数的用法。
|
21天前
|
存储 C++
【C++】string类的使用③(非成员函数重载Non-member function overloads)
这篇文章探讨了C++中`std::string`的`replace`和`swap`函数以及非成员函数重载。`replace`提供了多种方式替换字符串中的部分内容,包括使用字符串、子串、字符、字符数组和填充字符。`swap`函数用于交换两个`string`对象的内容,成员函数版本效率更高。非成员函数重载包括`operator+`实现字符串连接,关系运算符(如`==`, `&lt;`等)用于比较字符串,以及`swap`非成员函数。此外,还介绍了`getline`函数,用于按指定分隔符从输入流中读取字符串。文章强调了非成员函数在特定情况下的作用,并给出了多个示例代码。
|
1天前
|
C++
什么是析构函数,它在C++类中起什么作用
什么是析构函数,它在C++类中起什么作用?
19 11
|
1天前
|
C++
能不能说一个C++类的简单示例呀?能解释一下组成部分更好了
能不能说一个C++类的简单示例呀?能解释一下组成部分更好了
18 10
|
21天前
|
C++
【C++】string类的使用④(常量成员Member constants)
C++ `std::string` 的 `find_first_of`, `find_last_of`, `find_first_not_of`, `find_last_not_of` 函数分别用于从不同方向查找目标字符或子串。它们都返回匹配位置,未找到则返回 `npos`。`substr` 用于提取子字符串,`compare` 则提供更灵活的字符串比较。`npos` 是一个表示最大值的常量,用于标记未找到匹配的情况。示例代码展示了这些函数的实际应用,如替换元音、分割路径、查找非字母字符等。
|
21天前
|
C++
C++】string类的使用③(修改器Modifiers)
这篇博客探讨了C++ STL中`string`类的修改器和非成员函数重载。文章介绍了`operator+=`用于在字符串末尾追加内容,并展示了不同重载形式。`append`函数提供了更多追加选项,包括子串、字符数组、单个字符等。`push_back`和`pop_back`分别用于在末尾添加和移除一个字符。`assign`用于替换字符串内容,而`insert`允许在任意位置插入字符串或字符。最后,`erase`函数用于删除字符串中的部分内容。每个函数都配以代码示例和说明。
|
21天前
|
安全 编译器 C++
【C++】string类的使用②(元素获取Element access)
```markdown 探索C++ `string`方法:`clear()`保持容量不变使字符串变空;`empty()`检查长度是否为0;C++11的`shrink_to_fit()`尝试减少容量。`operator[]`和`at()`安全访问元素,越界时`at()`抛异常。`back()`和`front()`分别访问首尾元素。了解这些,轻松操作字符串!💡 ```