C++学习笔记(四)——类和对象(中)

简介: C++学习笔记(四)——类和对象(中)

1. 类的6个默认成员函数


如果一个类中什么成员都没有,简称为空类,空类中什么都没有?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。

class Date{};

image.png

注意:这里的“默认”和“缺省”的意思差不多,也就是你不写这6个函数,编译器会自动生成,你若是写了,则编译器就不生成了。

2. 构造函数


2.1 概念


概念:构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调

,保证每个数据成员都有 一个合适的初始值并且在对象的生命周期内只调用一次

2.2 特性


构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象而是初始化对象

其特征如下:

  • 函数名与类名相同
  • 无返回值
  • 对象实例化编译器自动调用对应的构造函数
  • 构造函数可以重载
class Date
{
public:
  // 构造函数
  // 1.无参构造函数
  Date()
  {
  }
  // 2. 有参构造
  Date(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  // 3.全缺省
  //Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
    //
private:
  int _year;
  int _month;
  int _day;
};
void test(){
  Date d1;//调用无参构造函数
  Date d2(2022,1,18);//调用带参的构造函数
}

注意:如果类中没有显示定义构造函数,则 C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再自动生成.

默认构造函数有三种:

  • 编译器默认生成的构造函数(对内置类型不会处理,还是随机值;对于自定义类型会去调用他们自己的构造函数初始化)
  • 全缺省构造函数
  • 无参构造函数

在我们不实现构造函数时,编译器就会调用编译器默认生成的构造函数,看下面一串代码,编译器的默认构造函数能否实现成员变量的初始化呢?

class Date
{
public:
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date  d1;
  d1.Print();
  return 0;
}

image.png

三个值都是随机值,编译器的默认构造函数并没有进行对成员变量进行初始化,是不是说这个函数就没有什么用呢?

解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如int/char...,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数.

class Time
{
public:
  Time()
  {
    cout << "Time()" << endl;
      _hour = 0;
    _minute = 0;
    _second = 0;
  }
private:
  int _hour;
  int _minute;
  int _second;
};
class Date
{
private:
  // 基本类型(内置类型)
  int _year;
  int _month;
  int _day;
  // 自定义类型
  Time _t;
};
int main()
{
  Date d;
  return 0;
}

image.png

总结:1.默认构造函数内置类型(int/char/double)不会处理,还是随机值;对于自定义类型(struct/class)会去调用他们自己的构造函数初始化。


2.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成.


3.一般情况建议都是写一个全缺省的构造函数,这种方式能够适应大多数场景。

3. 析构函数


3.1 概念


概念:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作.

3.2 特性


析构函数是特殊的成员函数

其特征如下:

  • 析构函数名是在类名前加上字符~
  • 无参数无返回值
  • 一个类有且仅有一个析构函数.若没有显示定义,则系统自动生成析构函数。
  • 对象声明周期结束时,C++编译系统会自动调用析构函数
typedef int DataType;
class SeqList
{
public :
SeqList (int capacity = 10)
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));
assert(_pData);
_size = 0;
_capacity = capacity;
}
~SeqList()//析构函数
{
if (_pData)
{
free(_pData ); // 释放堆上的空间
_pData = NULL; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private :
int* _pData ;
size_t _size;
size_t _capacity;
};
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
class Person
{
private:
String _name;
int _age;
};
int main()
{
Person p;
return 0;
}
//同样对于默认析构函数:对于内置类型成员不做处理,对于自定义类型会去调用他的析构函数

如果我们没有实现析构函数,编译器会自动生成一个析构函数,且这个析构函数对内置类型不处理,对自定义类型会调用它的析构函数,这一点和编译器生成的默认构造函数有点相似。

class A
{
public:
  A()
  {
    cout << "A()" << endl;
  }
  ~A()
  {
    cout << "~A()" << endl;
  }
private:
  int _a;
};
class B
{
public:
  B()
  {
    cout << "B()" << endl;
  }
  ~B()
  {
    cout << "~B()" << endl;
  }
private:
  int _b;
};
int main()
{
  A a;
  B b;
  return 0;
}
// 因为对象是定义在函数中,函数调用会建立栈帧,
  // 栈帧中的对象构造和析构也要很符合后进先出。
  // 所以这里的顺序是: A()->B()->~B()->~A()

image.png

4. 拷贝构造函数


4.1 概念


在创建对象时,可否创建一个与一个对象一某一样的新对象呢?

构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

4.2 特性


  • 拷贝构造函数是构造函数的一个重载形式;
  • 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用;
class Date {
public:
  Date(int year = 2020, int month = 2, int day = 20)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  // Date(Date d)
//若使用传值拷贝,则在传参时会先调用拷贝构造函数(因为传值拷贝时需先将原数据拷贝到临时空间再使用临时空间中的数据进行赋值),这样又会出现拷贝,这样就会陷入循环往复过程。   
   {
      _year=d._year;
      _month=d._month;
      _day=d._day;
     }//
  Date(const Date& d)//这里传参时也推荐加 const 因为被拷贝的对象是不应该被修改的
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
  void print()
  {
    cout << _year << "-" << _month << "-" << _day  << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
// 总结:
// 1.Date这样的类,需要的就是浅拷贝,那么默认生成的拷贝构造就够用了,不需要自己写
// 2.但是像Stack这样的类,需要的是深拷贝,浅拷贝会导致析构两次,程序就会崩溃等问题

传值引发无穷递归的原因:

image.png

若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

对自定义类型(Stack等),编译器还是会对他进行浅拷贝,这样就会将原来对象开辟空间的地址给拷贝了,这样的后果是:

1.两个对象共用一块空间,在调用析构函数时该空间会被释放两次

2.其中一个对象插入删除数据会导致另一个对象也插入删除了数据

所以像Stack这样的类,编译器默认生成的拷贝构造完成的是浅拷贝,不满足要求,需要实现深拷贝,这里不做过多介绍后面章节会分析.

class String
{
public:
  String(const char* str = "jack")
  {
    _str = (char*)malloc(strlen(str) + 1);
    strcpy(_str, str);
  }
  ~String()
  {
    cout << "~String()" << endl;
    free(_str);
  }
private:
  char* _str;
};
int main()
{
  String s1("hello");
  String s2(s1);
}

image.png

代码直接崩了,这就是浅拷贝带来的后果:相同内存被析构两次 

image.png

5. 赋值运算符重载


5.1 运算符重载


C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号

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

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型或者枚举类型的操作数
  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
  • 操作符有一个默认的形参this,限定为第一个形参

* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

代码示例:‘>’

class Date
{
public:
    Date(int year = 2022, int month = 2, int day = 20)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    // bool operator>(Date* this, const Date& d2)
    bool operator>(const Date& d2)
    {
    if (_year > d2._year)
  {
    return true;
  }
  else  if(_year == d2._year && _month > d2._month)
  {
    return true;
  }
  else if (_year == d2._year && _month == d2._month && _day > d2._day)
  {
    return true;
  }
  else {
    return false;
  }
    }
private:
    int _year;
    int _month;
    int _day;
};
void Test ()
{
    Date d1(2021, 1, 1);
    Date d2(2021, 1, 2);
    cout<<(d1 >d2)<<endl;// <<优先级高于==,这里需加括号
    //等于d1.operator>(d2)
}

5.1 赋值运算符重载


赋值运算符主要有以下五大点:

  • 参数类型
  • 返回值
  • 检测是否自己给自己赋值
  • 返回*this
  • 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
class Date
{
public:
  Date(int year = 2000, int month = 9, int day = 18)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  // 赋值运算符重载也是一个默认成员函数,也就是说我们不写编译器会自动生成
  // 编译器默认生成的赋值运算符重载跟拷贝构造的特性是一样的
  // 内置类型,会完成浅拷贝,自定义类型,会调用他的赋值运算符重载完成拷贝
  Date& operator=(const Date& d) // 有返回值是为了解决连续赋值的情况
  {
    if (this != &d)          //不是自己给自己赋值才需要拷贝
    {
      _year = d._year;            // 赋值运算符重载也是拷贝行为,不一样的是,
      _month = d._month;          // 拷贝构造是创建一个对象时,将同类对象初始化的拷贝。
      _day = d._day;              // 这里的赋值拷贝时两个对象都已经存在了,都被初始化过了
    }
    return *this;
  }
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
    Date d2(2022,2,20);
    d1=d2;//这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的
  Date d3;
  d3 = d2 = d1;//连续赋值
  d2.Print();
  d3.Print();
  Date d4 = d1; // 拷贝构造
  return 0;     // 拷贝构造:拿一个已经存在的对象去拷贝初始化另一个要创建的对象
}                 // 赋值重载:两个已经存在的对象拷贝

赋值运算符重载和拷贝构造函数的区别

相同:没有显示定义时,编译器都会默认生成一个,对于内置类型进行字节序的浅拷贝,对             自定义类型会调用它自身的拷贝构造函数或operator=。

不同:拷贝构造:用一个已经存在的对象初始化一个马上要创建的对象

          赋值重载:两个已经存在的对象拷贝

6. const成员


6.1 const修饰类的成员函数


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

image.png

class Date
{
public :
void Display () const
{
cout<<"Display () const" <<endl;
cout<<"year:" <<_year<< endl;
cout<<"month:" <<_month<< endl;
cout<<"day:" <<_day<< endl<<endl;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
void Test ()
{
Date d1 ;
d1.Display ();//非const可以调用const;const不能调用非const(权限不能放大)
const Date d2;//建议成员函数中,不需要改变成员变量,都加上const。
d2.Display ();
}

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


这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容

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

class Date {
public:
  Date(int year, int month, int day) {
    _year = year;
    _month = month;
    _day = day;
  }
  Date(const Date& d) {
    _year = d._year;
  }
  Date* operator&() {
    cout << "Date* operator&()" << endl;
    return this;
  }
  const Date* operator&() const {
    cout << "const Date* operator&() const" << endl;
    return this;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main() {
  Date d1(2019, 4, 1);
  const Date d2(2019, 3, 31);
  Date* pa1 = &d1;
  const Date* pd2 = &d2;
  system("pause");
  return 0;
}

7. 日期类的实现(对以上知识的运用,可以练练手)


Date.h

Date.h
#pragma once
#include<iostream>
using namespace std;
class Date {
public:
  Date(int year = 1, int month = 1, int day = 1);
  void print();
  int GetMonthDay(int year,int  month);//获取每个月的天数
  bool operator>(const Date& d);//比较第一个数是否比第二个数大
  bool operator<(const Date& d);//以下同理
  bool operator>=(const Date& d);
  bool operator<=(const Date& d);
  bool operator==(const Date& d);
  bool operator!=(const Date& d);
  //d1+=100
  Date& operator+=(int day);//在一个数本身加上天数,本身改变
  //d1+100
  Date operator+(int day);//一个数加上给定的天数是哪天?本身不改变
  //d1-=100
  Date& operator-=(int day);
    //d1-100
  Date operator-(int day);
  Date operator--(int);
  Date& operator--();
  Date operator++(int);//后置++,为了区分前置++与其构成函数重载,需要加int(规定,不允许是其他类型)
  Date& operator++();//前置++
  int operator-(const Date& d);//两个日期相减
  void PrintWeekDay() const;//打印今天是星期几
private:
  int _year;
  int _month;
  int _day;
};

Date.c

#include"Date.h"
Date::Date(int year, int month, int day)
{
  _year = year;
  _month = month;
  _day = day;
  if(!(_year >= 0 && (_month > 0 && _month < 13) && (_day>0 && _day <= GetMonthDay(year, month))))
  {
    cout << "非法日期" << endl;
  }
}
void Date::print()
{
  cout << _year << "-" << _month << "-" << _day << endl; 
}
int Date::GetMonthDay(int year, int month)
{
  int MonthDayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
  int day = MonthDayArray[month];
  if (month == 2 && (year % 4 == 0 && year % 400 != 0) || year % 100 == 0)
  {
    day += 1;
  }
  return day;
}
bool Date::operator>(const Date& d)
{
  if (_year > d._year)
  {
    return true;
  }
  else  if(_year == d._year && _month > d._month)
  {
    return true;
  }
  else if (_year == d._year && _month == d._month && _day > d._day)
  {
    return true;
  }
  else {
    return false;
  }
}
bool Date::operator==(const Date& d)
{
  if (_year == d._year && _month == d._month && _day == d._day)
  {
    return true;
  }
  else {
    return false;
  }
}
bool Date::operator>=(const Date& d)
{
  return (*this > d || *this == d);
}
bool Date::operator<(const Date& d)
{
  return !(*this >= d);
}
bool Date::operator<=(const Date& d)
{
  return !(*this > d);
}
bool Date::operator!=(const Date& d)
{
  return !(*this == d);
}
Date& Date::operator+=(int day)
{
  //2021-12-28 +=300
  _day += day;
  while ( _day> GetMonthDay(_year, _month))
  {
    _month += 1;
    _day -= GetMonthDay(_year, _month);
    while (_month == 13)
    {
      _year += 1;
      _month = 1;
    }
  }
  return *this;
}
Date Date::operator+(int day)
{
  Date ret(*this);
  ret += day;
  return ret;
}
Date& Date::operator-=(int day)
{
  //2021-1-20 -45
  _day -= day;
   while(_day <= 0)
  {
    _month--;
    if (_month == 0)
    {
      _year--;
      _month = 12;
    }
    _day += GetMonthDay(_year, _month);
  }
   return *this;
}
Date Date::operator - (int day)
{
  Date ret(*this);
  ret -= day;
  return ret;
}
//前置++
Date& Date::operator++()
{
  *this += 1;
  return *this;
}
Date Date::operator++(int)
{
  Date ret(*this);
  *this += 1;
  return ret;
}
Date& Date::operator--()
{
  *this -= 1;
  return *this;
}
Date Date::operator--(int)
{
  Date ret(*this);
  *this -= 1;
  return ret;
}
int Date::operator-(const Date& d)
{
  Date max = *this;
     Date min = d;
   int flag = 1;
   if (*this < d)
   {
     max = d;
     min = *this;
     flag = -1;
   }
   int count = 0;
   while (min != max)
   {
     min++;
     count++;
   }
   return flag * count;
}
void Date::PrintWeekDay() const
{
  const char* arr[] = { "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天" };
  Date start(1900, 1, 1);
  int count = *this - start;
  cout << arr[count % 7] << endl;
}

以上是类和对象的部分内容,有不足的地方或者对代码有更好的见解,欢迎评论区留言共同商讨,共同进步!!

image.png

相关文章
|
19天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
29 2
|
25天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
59 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
65 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
74 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
30 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
26 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
25 1
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
2月前
|
存储 编译器 C语言
【C++类和对象(上)】—— 我与C++的不解之缘(三)
【C++类和对象(上)】—— 我与C++的不解之缘(三)