C++类和对象之类的6个默认成员函数详解

作者:榶曲 时间:2022-01-05 13:50:35 

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

默认成员函数:用户没有显示实现,编译器会生成的成员函数称为默认成员函数。

如果一个类中什么成员都没有,简称为空类。

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

class Date{};
  • 构造函数: 完成初始化工作

  • 析构函数: 完成清理工作

  • 拷贝构造函数: 使用同类对象初始化创建对象

  • 赋值重载: 把一个对象赋值给另一个对象

  • 取地址操作符重载: 对普通对象取地址

  • const取地址操作符重载: 对const修饰的对象取地址

C++类和对象之类的6个默认成员函数详解

2.构造函数

2.1概念

对于下面的Date类:

class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};

int main()
{
Date today1;
today1.Init(2023,1,16);
today1.Print();

Date today2;
today2.Init(2023, 1, 17);
today2.Print();

return 0;
}

对于Date类,可以通过Init公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

我们就需要一个函数:保证对象被创造出来就被初始化了。

C++的构造函数提供了这个功能:

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。

2.2特性

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

其特征如下:

  1. 函数名与类名相同。

  2. 无返回值(也不用写void)

  3. 对象实例化时编译器自动调用对应的构造函数。

C++类和对象之类的6个默认成员函数详解

构造函数可以重载。(一个类可以有多个构造函数)

class Date
{
public:
Date()
{
cout << "自定义默认构造函数" << endl;
}
//Date(int year = 1, int month= 2, int day = 3)
//{
//cout << "自定义全缺省默认构造函数" << endl;
//}
//Date(int year, int month, int day = 1)
//{
//cout << "自定义半缺省构造函数" << endl;
//}
Date(int year, int month, int day)
{
cout << "自定义构造函数" << endl;
}
private:
int _year;
int _month;
int _day;
};

int main()
{
Date today(2023, 2, 6);

return 0;
}

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数一个类中只能有一个。

原因: 这两个构造函数虽然满足重载,但编译器无法调用,存在歧义。如上面代码的第一个无参构造函数和第二个注释的全缺省的构造函数,所以默认构造函数一个类只能有一个。(非默认构造函数也只能有一个,如第三个半缺省构造函数和第四个构造函数,需要传参,同时存在会有歧义)

注意: 更具构造函数需不需要传参数,我们将其分为两种

  • 默认构造函数: 无参构造函数、全缺省构造函数,我们没写编译器默认生成的构造函数(下一条)(这些不需要传参数的构造函数,都认为是默认构造函数)

  • 传参构造函数: 不缺省构造函数、全缺省构造函数

    • 创建对象时,调用默认构造函数不要在对象后加括号(加括号后编译器会将其看作函数的声明,而不是创建的对象)。调用传参的构造函数在对象后加括号加参数。

如果类没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成。(构造函数可以重载,有很多种,只要我们写一种,编译器就不会默认生成构造函数)

注意: 如下,创建对象时不带括号,调用的是默认构造函数,带括号后跟参数,调用传参构造函数。

如下图类中已经定义了构造函数,编译器不会在自动生成默认构造函数。

C++类和对象之类的6个默认成员函数详解

在增加默认构造函数后,正常运行

C++类和对象之类的6个默认成员函数详解

关于编译器生成的默认构造函数,很多人会疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?

如下面的代码,today对象调用了编译器生成的默认构造函数,但是today对象的三个成员变量_day/_month/_year,依然是随机数,也就是说在这里编译器生成的默认构造函数并没有什么用?

C++类和对象之类的6个默认成员函数详解

先介绍一点,C++将类型分为以下两种:

  • 内置类型: 语言提供的数据类型,如:int、char&hellip;

  • 自定义类型: 我们使用struct、class、union等自己定义的类型

如果一个类中存在自定义类型的成员变量,需要使用该成员变量对应类的默认构造函数来初始化,否则无法通过。这也就是默认构造函数存在的意义。

自定义类型的成员变量对应类存在默认构造函数

class A
{
public:
A()
{
cout << "A" << endl;
}
private:
int a;
int b;
};

class Date
{
public:
Date()
{
cout << "默认构造函数" << endl;
}
Date(int year, int month, int day)
{
cout << "传参构造函数" << endl;
}
private:
int _year;
int _month;
int _day;
A a1;
};

int main()
{
Date today;

return 0;
}

C++类和对象之类的6个默认成员函数详解

自定义类型的成员变量对应类不存在默认构造函数

class A
{
public:
A(int c)
{
cout << "A" << endl;
}
private:
int a;
int b;
};

class Date
{
public:
Date()
{
cout << "默认构造函数" << endl;
}
Date(int year, int month, int day)
{
cout << "传参构造函数" << endl;
}
private:
int _year;
int _month;
int _day;
A a1;
};

int main()
{
Date today;

return 0;
}

C++类和对象之类的6个默认成员函数详解

注意: 在C++11中针对内置成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

class A
{
public:
void Print()
{
cout << _a << " " << _b << endl;
}
private:
int _a = 10;
int _b = 20;
};

class Date
{
public:
void Print()
{
a1.Print();
}

private:
int _year = 2000;
int _month = 1;
int _day = 1;
A a1;
};
int main()
{
Date today;
today.Print();

return 0;
}

C++类和对象之类的6个默认成员函数详解

此为缺省值,当构造函数没有初始化成员变量时,成员变量的值即为该缺省值,若初始化,以构造函数为主(如下面代码,初始化了一个变量,该变量就以构造函数初始化为主,其他成员变量为缺省值)

class A
{
public:
A()
{
_a = 40;
}
void Print()
{
cout << _a << " " << _b << endl;
}
private:
int _a = 10;
int _b = 20;
};

class Date
{
public:
void Print()
{
a1.Print();
}

private:
int _year = 2000;
int _month = 1;
int _day = 1;
A a1;
};
int main()
{
Date today;
today.Print();

return 0;
}

C++类和对象之类的6个默认成员函数详解

3.析构函数

3.1概念

我们知道了对象创建时需要构造函数来初始化,那对象销毁时又需要什么呢?

析构函数: 与构造函数相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时自动调用析构函数,完成对象中资源的清理工作。

我们创建一个对象,它是在对象生命周期结束后,对应函数的栈帧销毁时一并销毁,而析构函数是在销毁前函数自动调用,对该对象的资源做清理清空对象的空间或将申请的空间还给编译器。

对于清理工作,我们必须要做,否则可能会造成内存泄漏,而我们又常常忘记这一操作,于是C++增加了这么一个函数。

3.2特性

  1. 析构函数名是在类名前加上字符**~**(取反符号)

  2. 无参数也无返回值

  3. 一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。注意:析构函数不能重载

  4. 对象生命周期结束时,C++编译系统自动调用析构函数。

我们编写如下代码,向内存申请空间,利用析构函数释放对应的空间。

class Stack
{
public:
Stack()
{
ArrStack = (int*)malloc(sizeof(int) * 4);
       if(!ArrStack)//下图中未写
       {
           preeor("malloc fail!");
           exit(-1);
       }
_size = 4;
_top = 0;
}
~Stack()
{
if (ArrStack)
{
free(ArrStack);
ArrStack = nullptr;
_size = 0;
_top = 0;
}
}
private:
int* ArrStack;
int _size;
int _top;
};

int main()
{
Stack st;

return 0;
}

C++类和对象之类的6个默认成员函数详解

如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏。

如下面的代码,当我们对同一个类创建两个变量时,构造函数的执行顺序为:s1、s2,而函数是一种栈的形式,创建变量就是压栈,s1先入栈,s2后入栈,销毁时,s2先出栈,s1后出栈,析构函数的调用顺序为:s2、s1

class Stack
{
public:
Stack(int num)
{
ArrStack = (int*)malloc(sizeof(int) * num);
       if(!ArrStack)//下图中未写
       {
           preeor("malloc fail!");
           exit(-1);
       }
_size = 4;
_top = 0;
}
~Stack()
{
if (ArrStack)
{
free(ArrStack);
ArrStack = nullptr;
_size = 0;
_top = 0;
}
}
private:
int* ArrStack;
int _size;
int _top;
};

int main()
{
Stack s1(10);
Stack s1(40);

return 0;
}

观察下图this->_size的变化

C++类和对象之类的6个默认成员函数详解

当一个类中有自定义类型的成员变量,那再销毁这个类创建的对象时,会调用该类中自定义类型的成员变量的析构函数

写析构函数

class A
{
public:
~A()
{
cout << "A" << endl;
}
private:
int a;
int b;
};

class Date
{
public:
~Date()
{
cout << "Date" << endl;
}
private:
int _year;
int _month;
int _day;
A a1;
};
int main()
{
Date today;

return 0;
}

C++类和对象之类的6个默认成员函数详解

不写析构函数

class A
{
public:
~A()
{
cout << "A" << endl;
}
private:
int a;
int b;
};

class Date
{
public:

private:
int _year;
int _month;
int _day;
A a1;
};
int main()
{
Date today;

return 0;
}

C++类和对象之类的6个默认成员函数详解

注意:

  • 默认生成构造函数和默认生成析构函数,对内置类型不处理,处理自定义类型。(有些编译器会,但那时编译器的个人行为,和C++的语法无关)

4.拷贝构造函数

4.1概念

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

该函数功能为将一个对象的数据赋值给另一个对象,发生拷贝时编译器就会调用该函数,如下:

class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "拷贝构造函数" << endl;
}

private:
int _year;
int _month;
int _day;
};

void test(Date d)//调用拷贝构造函数
{}

int main()
{
Date today1(2023,2,7);
Date today2(today1);//调用拷贝构造函数

test(today1);

return 0;
}

C++类和对象之类的6个默认成员函数详解

4.2特征

拷贝构造函数是构造函数的一个重载形式。

拷贝构造函数的参数只有一个必须是类类型对象的引用 ,使用传参方式编译器会直接报错 ,因为会引发无穷递归调用。

如果不使用引用,代码如下:

class Date
{
public:
Date(const Date d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}

private:
int _year;
int _month;
int _day;
};

这样的拷贝构造函数,我们在调用它时会发生拷贝,而需要拷贝我们就要调用拷贝构造函数,这就会形参死循环,因为要用你我调用你,而想要调用你就要用你,编译器不会允许这样的事情发生。

C++类和对象之类的6个默认成员函数详解

如上图,将对象d1的数据拷贝到d2,需要调用拷贝构造函数,而调用的过程形参发生拷贝又要调用拷贝构造函数,就这样一直下去,很明显这是不行的。

所以在这里我们要使用引用,如下:

Date(const Date& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}

在第一次调用的时候,使用d给对象起别名,就不用再调用其他拷贝构造函数。

对于这个函数建议使用const修饰,防止我们在写这个函数时不小心写错,使对象的成员变量发生改变。

**若未显示定义,编译器会生成默认的拷贝构造函数。**默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫浅拷贝,或值拷贝。

即上面的Date类对象,若发生浅拷贝只是将一个对象所占空间内所有成员变量的值拷贝到另一个对象的成员变量,这么做看起来似乎很合理其实不然,对于内置类型,这么做当然没有问题,但如栈这样的数据结构,是万万不能的。如下面栈的代码

class Stack
{
public:
   Stack(size_t capacity = 10)
   {
       _array = (int*)malloc(int* sizeof(int));
       if (nullptr == _array)
       {
           perror("malloc申请空间失败");
           return;
       }
       _size = 0;
       _capacity = capacity;
   }
   void Push(const DataType& data)
   {
       // CheckCapacity();
       _array[_size] = data;
       _size++;
   }
   ~Stack()
   {
       if (_array)
       {
           free(_array);
           _array = nullptr;
           _capacity = 0;
           _size = 0;
       }
   }
private:
   int *_array;
   size_t _size;
   size_t _capacity;
};
int main()
{
   Stack s1;
   s1.Push(1);
   s1.Push(2);
   s1.Push(3);
   s1.Push(4);
   Stack s2(s1);
   return 0;
}

C++类和对象之类的6个默认成员函数详解

这样程序必定会发生错误。

如果想要让程序正确运行我们,需要我们自己编写拷贝构造函数,也就是深拷贝,让他们每个对象的的成员变量在面对这种情况时都有自己独立的空间,而不是共用一块空间。

这也是拷贝构造函数存在的意义,编译器只能做浅拷贝的工作,若果一个对象的拷贝需要使用深拷贝,就需要程序员手动来完成这个任务,这也是C语言存在的缺陷,C++的很好的弥补了这一点。

修改后的栈代码如下:

class Stack
{
public:
   Stack(size_t capacity = 10)
   {
       _array = (int*)malloc(capacity * sizeof(int));
       if (nullptr == _array)
       {
           perror("malloc申请空间失败");
           return;
       }
       _size = 0;
       _capacity = capacity;
   }
   Stack(const Stack& st)
   {
       _array = (int*)malloc(sizeof(int) * st._capacity);
       if (_array == nullptr)
       {
           perror("malloc申请空间失败");
           return;
       }
       for (int i = 0; i < st._size; i++)
       {
           _array[i] = st._array[i];
       }
       _size = st._size;
   }
   void Push(const int& data)
   {
       // CheckCapacity();
       _array[_size] = data;
       _size++;
   }
   ~Stack()
   {
       if (_array)
       {
           free(_array);
           _array = nullptr;
           _capacity = 0;
           _size = 0;
       }
   }
private:
   int* _array;
   size_t _size;
   size_t _capacity;
};
int main()
{
   Stack s1;
   s1.Push(1);
   s1.Push(2);
   s1.Push(3);
   s1.Push(4);
   Stack s2(s1);
   return 0;
}

所以如果类中没有涉及资源申请时,拷贝构造函数是否写都可以,若涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

拷贝构造函数调用频率最多的三种场景场景如下

  • 使用以存在的对象创建新对象

  • 函数参数类型为类类型对象

  • 函数返回值类型为类类型对象

C++类和对象之类的6个默认成员函数详解

通过这些我们也可以看出,拷贝在编写代码中是一个平常的事情,但其消耗的资源却不少,所以在实际使用中,如果可以使用引用,尽量使用引用,减少计算机消耗,创出更优得程序。

5.赋值运算符重载

5.1运算符重载

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

在C++中类的封装性是做的很好的,如果想在类和类之间进行比较,拷贝等操作需要在类内调用函数,而对应普通的内置类型,只需要使用简单的运算符即可完成,C++规定可以将部分运算符重载来完成这个功能,增强了代码的可读性。

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

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

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@

  • 重载操作符必须有一个类类型参数

  • 用于内置类型的运算符,其含义不能改变,例如:内置的整形+,不能改变其含义

  • 作为类成员函数重载时,其形参看起来比操作数数目少1.因为成员函数的第一个参数为隐藏的this

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

如下代码,若运算符重载函数作用域为全局,那类的成员变量必须为公有的,这样封装性就无法保证

class Date
{
public:

Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}

//成员变量变为公有,才能使类外访问
//private:
int _year;
int _month;
int _day;
};

bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year && d1._month == d2._month
&& d1._day == d2._day;
}

bool test()
{
Date today1(2023, 2, 7);
Date today2;
return today1 == today2;
}

这里我们可以使用友元解决,也可以将运算符重载函数放入类中,我们一般将其放入类中。

class Date
{
public:

Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d1)
{
return _year == d1._year && _month == d1._month
&& _day == d1._day;
}

private:
int _year;
int _month;
int _day;
};

bool test()
{
Date today1(2023, 2, 7);
Date today2;
   //today1.operator==(today2)
return today1 == today2;
}

在调用成员函数时,编译器会自动将调用的对象作为this指针传递,我们只要写入一个参数即可。

注意:

在使用时需要注意运算符优先级,如下面使用运算符重载需使用括号

cout << (today1 == today2) << endl;

运算符重载中,如果有多个参数,第一参数为左操作数,第二个参数为右操作数,以此类推。如上面的代码,第一个参数为today1,为左操作数,由该对象调用运算符重载函数,第二参数today2即为参数。

5.2赋值运算符重载

赋值运算符如果不自己实现,编译器会默认生成,只有赋值取地址是这样的,其它的自定义类型需要使用,就要我们自己写。(取地址在下面)

赋值运算符重载格式:

  • 参数类型: const Typedef&,传递引用可以提高传参效率

  • 返回值类型: Typedef&,返回引用可以提高返回得效率,有返回值目的是为了支持连续赋值。

  • 检测是否自己给自己赋值

  • **返回*this:**要符合连续赋值得含义

class Date
{
public:

Date(int year = 2000, int month = 1, int day = 1)
{
_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;//返回*this
}

private:
int _year;
int _month;
int _day;
};

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

class Date
{
public:

Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}

//private:
int _year;
int _month;
int _day;
};

//全局函数不能用`this`指针,需要给两个参数
Date& operator=(const Date& d1, const Date& d2)
{
   if (d1 != &d2)//检测是否自己给自己赋值
   {
       d1._year = d2._year;
       d1._month = d2._month;
       d1._day = d2._day;
   }
return d1;//返回*this
}

其中为了访问类中得成员变量,将其公有化,失去了封装性。这样得函数注定编译失败,其中赋值运算符没有实现,则编译器会在类中自己实现一个默认的赋值运算符,而在调用得时候,我们自己实现了一个,编译器又实现了一个这就产生冲突。

所以,赋值运算符重载只能是类的成员函数。

上面已经讲了,如果我们没有自己写,编译器会自己实现一个默认的赋值运算符重载,在运行是是以值得方式逐字节拷贝。 上面得拷贝构造函数中,编译器自己默认创建的拷贝构造函数也是相同的,只能进行浅拷贝,只能拷贝值无法为其分配内存,但赋值运算符重载还是有一点不同的,它初始化需要分配空间的时候,会先为创建的对象分配空间,之后在使用赋值运算符,将分配好的空间舍弃,存入其他对象的空间地址。

如下代码:

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (int*)malloc(capacity * sizeof(int));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const int& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
int* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2;
s2 = s1;
return 0;
}

C++类和对象之类的6个默认成员函数详解

我们要注意,如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

5.3前置++和后置++重载

对于前置++,我们按照正常的运算符重载模式写即可,但记得返回类型需要使用类类型&,将修改后的对象返回。

class Date
{
public:

Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator++()
{
_year += 1;
return *this;
}

private:
int _year;
int _month;
int _day;
};
int main()
{
Date today(2023, 2, 7);
Date d;
d = ++today; //d:2024.2,7  today:2024,2,7

return 0;
}

至于后置++,为了可以让两个函数实现重载,规定增加一个int类型的参数,作为区分。

注意:前置++是先++后使用,所以可以直接返回其修改后的对象,对于后置++是先使用后++,所以返回的应该是未修改的对象,我们可以在修改原对象前对其进行拷贝,然后修改原对象,返回时直接返回之前拷贝的对象,这样原对象即改变了,使用的也是未改变的对象,符合后置++

class Date
{
public:

Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date operator++()
{
Date& temp(*this);
_year += 1;
return temp;
}

private:
int _year;
int _month;
int _day;
};

int main()
{
Date today(2023, 2, 7);
Date d;
d = today++; //d:2023,2,7  today:2024,2,7

return 0;
}

5.4流插入和流提取运算符重载

在C++中,我们输出和输入一个数据通常是通过cout、cin,它们两其实就是一个类对象,重载了<<、>>两个运算符,所以输入、输出其实就是调用两个运算符重载函数。

C++类和对象之类的6个默认成员函数详解

如上图,它们的类型分别为ostream、istream,存放在iostream这个头文件中,而C++库内定义的东西都存放在std这个命名空间内,所以我们每次开头需要写这两行代码。

对于内置类型,如下:

int a = 10;
double b = 10.0;
cout << a;
cout << b;

通过函数重载调用不同的运算符函数,将其打印。

下面我们一起来看一下这两个运算符是如何重载的。

流提取

在类中定义:

class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void operator<<(ostream& out)
{
       //下面就是输出内置类型的值,流提取调用头文件<iostream>内的
out << _year << "年" << _month << "月" << _day << "日" << endl;
}

private:
int _year;
int _month;
int _day;
};

int main()
{
Date today;
   //第一个参数为左操作数,第二个参数为右操作数,由创建的对象调用类内的重载函数
   //today.operator<<(cout)
today << cout;

return 0;
}

C++类和对象之类的6个默认成员函数详解

我们看到,函数的使用形式是today << cout;,类对象抢占第一个参数,一定在左边,cout在右边,这么写肯定不符合我们平常的习惯,如果要将cout放在第一个位置,我们需要将函数在全局定义。

class Date
{
public:
friend ostream& operator<<(ostream& out, const Date& d);
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}

private:
int _year;
int _month;
int _day;
};

//不对对象的成员变量做修改,最好使用const修饰,防止写错,发生错误
ostream& operator<<(ostream& out,const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
   return out;
}

int main()
{
Date today;
cout << today;

return 0;
}

C++类和对象之类的6个默认成员函数详解

如上面的代码,我们函数变为全局后,很好的解决了位置的问题,但我们又无法访问类中的成员变量,这里有三种方法,我们使用第一种

使该函数变为类的友元函数,在类中public作用域下,使用friend修饰函数的声明,即可在该函数内使用对应类的对象调用成员变量。增加接口,在类中创建输出函数,调用对应函数即可得到对应的成员变量值,对象在类外无法访问成员变量,但可以访问对外开发的函数。(java喜欢这么做)删除private作用域,这样成员变量就可以访问。(不建议这么做,破坏封装性)

为了防止出现下面的情况,以此要输出多个对象的值,我们需要使重载的函数返回cout,使函数可以正常运行。

cout << d1 << d2 << d3 << endl;
//cout << d1  //调用重载函数,调用后返回cout继续执行
//cout << d2  //同时,运行后返回cout
//..
//cout << endl; //与重载的类型不匹配,调用头文件内的函数

流插入

class Date
{
public:
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);

Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}

private:
int _year;
int _month;
int _day;
};

ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}

//需要改变对象的成员变量,不能使用const修饰
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}

int main()
{
Date today;

cin >> today;
cout << today;

return 0;
}

C++类和对象之类的6个默认成员函数详解

如上面的代码与流提取相似。

6.const成员

如下面的代码,是否可以正常运行呢?

class Date
{
public:
Date(int year=2000,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print" << endl;
}
private:
int _year;
int _month;
int _day;
};

int main()
{
const Date d;
d.Print();

return 0;
}

它不能正常运行,因为对象d使用const修饰了,它的值是无法改的(该对象的成员变量无法修改)。在调用成员函数时,编译器默认传过去的值为Date* const this,this指针表示对象本身,意味着在此函数内成员变量可以改变,这产生了冲突。更简单的说,这就是将原本只能读的对象变成可读可写,无视其权限。

想要解决这个问题,只要使用const修饰*this使其无法改变即可,而this又是编译器默认的,它是被隐藏着的不好修改,C++给出了如下方法,在成员函数的括号后直接加const即表示修饰*this,如下

void Print() const
{
cout << "Print" << endl;
}

如果我们使用为被修饰的const对象调用被const修饰的成员函数,这时可以的,原本对象就可以通过调用成员函数修改和读取,现在只是传过去只能使成员函数读取这没有问题。

class Date
{
public:
Date(int year=2000,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
cout << "Print" << endl;
}
private:
int _year;
int _month;
int _day;
};

int main()
{
Date d;
d.Print();

return 0;
}

同理:在类中成员函数是可以相互调用的,但被const修饰的成员函数无法调用没有被修饰的,因为被修饰的成员函数所函数*this指针是无法改变的,而没有被修饰的是可以改变的,const失去了作用,这种写法是错误的。而没有被修饰的成员函数是可以调用被修饰的,这属于即可读又可写的情况向只可读的情况发展,没有改变语法。

注意:

类内部不改变成员变量的成员函数,最好加上const,防止数据被修改

一般会在下面的场景用到const成员

class Date
{
public:
Date(int year=2000,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
cout << _year << endl;
}
private:
int _year;
int _month;
int _day;
};

void test(const Date& d)
{
d.Print();
}

int main()
{
Date td;
test(td);

return 0;
}

我们在创建对象之初一般不为其修饰cosnt,但我们会经常将对象作为实参传递给其他函数,如果形参被const修饰,那在这个函数内它只能被读,无法修改意味着调用的成员函数也必须被const修饰。

const这种写法只针对成员函数

若定义和声明分离,需要修饰const时,定义和声明都要修饰

成员函数被const修饰和不被修饰构成const重载

void Print() const
{
cout << _year << endl;
}
void Print()
{
cout << _year << endl;
}

一个形参为Date* const this,一个为const Date* const this,形参不同满足重载

若是成员函数被const修饰注意它的返回值类型,若返回的是成员变量,也需要修饰const,否则权限发生变化,编译会出错

7.取地址重载和const取地址操作符重载

取地址重载和const取地址操作符重载是最后两个编译器默认生成的成员函数,我们一般不会去写它,而是直接去使用编译器默认生成的。

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

int main()
{
Date d1;
const Date d2;
cout << &d1 << endl;
cout << &d2 << endl;

return 0;
}

C++类和对象之类的6个默认成员函数详解

我们如果想要写出来也可以,如下:

class Date
{
public:
Date(int year=2000,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date* operator&()//取地址重载
{
return this;
}
const Date* operator&() const //const取地址操作符重载
{
return this;
}
private:
int _year;
int _month;
int _day;
};

int main()
{
Date d1;
const Date d2;
cout << &d1 << endl;
cout << &d2 << endl;

return 0;
}

C++类和对象之类的6个默认成员函数详解

两个使用的场景不同,取地址重载用在取一般的对象的地址,const取地址操作符重载用在取被const修饰的对象的地址。

来源:https://blog.csdn.net/m0_52094687/article/details/128958076

标签:C++,类,成员函数
0
投稿

猜你喜欢

  • Android SearchView搜索控件使用方法详解

    2022-07-09 16:49:21
  • Android 开发之BottomBar+ViewPager+Fragment实现炫酷的底部导航效果

    2022-01-07 19:39:20
  • apache ant进行zip解压缩操作示例分享

    2021-11-08 09:16:03
  • Java线程生命周期的终止与复位

    2022-04-22 01:57:01
  • C#中使用ADOMD.NET查询多维数据集的实现方法

    2023-10-27 05:38:15
  • 使用@ConfigurationProperties实现类型安全的配置过程

    2023-07-01 00:26:05
  • Java中BigDecimal类的add()的使用详解

    2023-03-07 16:12:11
  • SpringBoot+hutool实现图片验证码

    2021-06-17 02:55:27
  • C#简单聊天室雏形

    2023-02-27 16:14:13
  • Android仿360桌面手机卫士悬浮窗效果

    2021-06-16 10:06:37
  • Java里的static import使用小结

    2023-08-18 18:36:36
  • spring mvc中的@ModelAttribute注解示例介绍

    2023-10-15 07:07:06
  • MyBatis快速入门

    2023-11-13 06:45:14
  • C#巧用DateTime预设可选的日期范围(如本年度、本季度、本月等)

    2022-09-19 11:06:41
  • kotlin gson反序列化默认值失效深入讲解

    2022-04-07 15:28:59
  • Android中执行java命令的方法及java代码执行并解析shell命令

    2022-08-27 15:45:13
  • Spring/SpringBoot @RequestParam注解无法读取application/json格式数据问题解决

    2023-11-26 11:26:29
  • java排序算法之冒泡排序

    2023-04-05 21:03:42
  • Android百度定位导航之基于百度地图移动获取位置和自动定位

    2022-10-21 10:50:18
  • Spring Boot读取配置文件内容的3种方式(@Value、Environment和@ConfigurationProperties)

    2022-09-24 05:52:17
  • asp之家 软件编程 m.aspxhome.com