C++之类和对象——中篇

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

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

class C {};

C++之类和对象——中篇_第1张图片

取地址重载相对上面两个并不是很重要。

二. 构造函数

1. 概念

对于以下的日期类:

class Date
{ 
public:
     void SetDate(int year, int month, int day)
     {
         _year = year;
         _month = month;
         _day = day;
     }
 
     void Display()
     {
         cout <<_year<< "-" <<_month << "-"<< _day <

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

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

2. 特性

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

其特征如下:

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

void TestDate()
{
     Date d1; // 调用无参构造函数
     Date d2 (2015, 1, 1); // 调用带参的构造函数
     Date d3(); // 声明d3函数
}

注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明,以上代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象

  • 如果类中没有显式定义构造函数(拷贝构造函数也算构造函数!),则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date
{
public:
     /*
     // 如果用户显式定义了构造函数,编译器将不再生成
     Date (int year, int month, int day)
     {
     _year = year;
     _month = month;
     _day = day;
     }
     */
private:
     int _year;
     int _month;
     int _day;
};
void Test()
{
     // 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
     Date d;
}

关于编译器生成的默认构造函数,会有疑问:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象year/month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用吗?

其实不然,C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如 int/char...,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,并且运行一下

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;
}

就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认构造函数,总结起来就是:默认生成的构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理处理方式为调用自定义类型里的默认构造函数。一般是不会使用编译器的默认构造函数,只有在自定义类型的情况才会让编译器去调用默认构造函数,因为此时调用的默认构造函数就是自定义类型里的默认构造函数

值得一提的是,如果自定义类型里都没写默认构造函数,即使是让编译器去调用默认构造函数,那也是随机值。但是,如果在自定义类型里写了构造函数,而不是默认构造函数并且你还想让编译自己生成默认构造函数,那就报错咯,如下图:

C++之类和对象——中篇_第2张图片

由于在自定义类型里写构造函数,又想让编译器去使用自定义里的默认构造函数,编译器找不到,自然就报错了

毕竟编译器对自定义类型成员变量也只是调用自定义类型的默认构造函数而已。所以,说了这么多,构造函数一般还是要自己写,这才是重点!哦对了,顺带一提,别想着自己写一个在自定义类型外的本应该在自定义类型的构造函数,因为你压根访问不了自定义类型里的成员变量。

  • 无参的构造函数和全缺省的构造函数都称为默认构造函数(不用传参就可以调用的构造函数),并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都是默认构造函数。

构造函数在不进行调用的情况下进行编译如果存在多个构造函数,编译是会通过的,但是调用就会报错,因为当出现全缺省构造函数和无参构造函数时,是会出现歧义的:

C++之类和对象——中篇_第3张图片

C++之类和对象——中篇_第4张图片这里如果是d1要调用构造函数,就会出现歧义,不知道调用谁,所以一般情况构造函数都会写成全缺省构造函数 

PS:C++11就内置类型不处理,只处理自定义类型处理的问题进行了解决,可以在成员变量赋值,如下图:

C++之类和对象——中篇_第5张图片

需要注意的是这里的赋值并不是初始化(因为这里只是成员变量的声明,并不是定义,所以应该叫缺省值才对),给的是缺省值,供编译器自己生成默认构造函数使用

结论:如果一个类中的成员全是自定义类型并且这些成员都提供了默认构造函数,我们就可以使用默认生成的构造函数,如果有内置类型的成员,声明时给了缺省值,也可以使用默认生成的构造函数,否则要自己实现构造函数,或者需要显示传参初始化,要自己实现构造函数。

PS:成员变量的命名风格

// 我们看看这个函数,是不是很奇怪
class Date
{
public:
     Date(int year)
     {
         // 这里的year到底是成员变量,还是函数形参?
         year = year;
     }
private:
     int year;
};
// 所以我们一般都建议这样
class Date
{
public:
     Date(int year)
     {
         _year = year;
     }
private:
     int _year;
};
// 或者这样。
class Date
{
public:
     Date(int year)
     {
         m_year = year;
     }
private:
     int m_year;
};
// 其他方式也可以的,一般都是加个前缀或者后缀标识区分就行。

三. 析构函数

1. 概念

前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没的? 析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。(可以类比栈的销毁,清理像malloc,new出来的空间)

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)
         {
             cout << "~SeqList:" << this << endl;
             free(_pData ); // 释放堆上的空间
             _pData = NULL; // 将指针置为空
             _capacity = 0;
             _size = 0;
         }
     }
 
private :
     int* _pData ;
     size_t _size;
     size_t _capacity;
};

C++之类和对象——中篇_第6张图片

通过对this指针的打印我们会发现,这里的确是Data默认生成的析构函数调用了自定义类型里的析构函数,因为声明了两个,所以也就是调用了两次

关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对内置类型不做处理,会对自定类型成员调用它的析构函数。(与构造函数相似)

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;
}

析构函数和构造函数的顺序问题:栈区里面定义的对象,析构顺序和构造顺序是反的,毕竟栈就是后进先出。

四. 拷贝构造函数

1. 概念

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

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

2. 特征

拷贝构造函数也是特殊的成员函数,其特征如下:

  • 拷贝构造函数是构造函数的一个重载形式
  • 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
class Date
{
public:
     Date(int year = 1900, int month = 1, int day = 1)
     {
         _year = year;
         _month = month;
         _day = day;
     }
     Date(const Date& d)
     {
         _year = d._year;
         _month = d._month;
         _day = d._day;
     }
private:
     int _year;
     int _month;
     int _day;
};
int main()
{
     Date d1;
     Date d2(d1);
     return 0;
}

函数调用传参对于同类型自定义类型的对象传值传参需要调用拷贝构造函数,如下图:

C++之类和对象——中篇_第7张图片

我们会发现,我们本来是想调用Func函数,但是这里因为传参传的是同类型自定义类型的对象,就需要先调用拷贝构造函数,在调试下我们会发现程序会运行到Date(const Date& d)拷贝构造函数处,先构造出(复制出一个d1)d,因为传值传参中形参是实参的一份拷贝,所以我们需要先调用拷贝构造函数拷贝构造一份d1。如果不想调用拷贝构造函数,就给Func传引用就不会调用拷贝函数了,因为此时的d是d1的别名,传的是引用不是值,不需要构造一份新的。

这里也能理解为什么会发生无穷递归了,函数调用传参传同类型自定义类型的参数如果不是传引用是传值就需要调用拷贝构造函数再调用函数,我们传参使用的传值传参,因为函数调用需要先传参,而传的是值,就需要先调用拷贝构造函数,而我们的拷贝构造函数如果是传值传参的方式实现的,那么这个拷贝构造函数因为需要先传参,由于这个拷贝构造函数传参使用的是传值,又需要去调用拷贝构造函数,但是,拷贝构造函数我们使用的传值传参,需要调用拷贝构造函数,依次一直往下,最后就是无穷递归了。

PS:可能会想到拷贝构造函数使用指针方式,原则上指针是可以的,但是指针传参的时候由于传的是地址,并不是对象,那此时的函数只是普通的构造函数,并不是拷贝构造函数,因为语法规定传对象才是拷贝构造,那传地址自然就变成普通的构造函数了。

PS:以上我们所接触的拷贝构造其实是一种浅拷贝,直接将值给拷贝构造,但是这种方式其实在某些情况容易出问题,如下面的代码:

class Stack {
public:
    //构造
    Stack(int capacity = 10)
    {
        _a = (int*)malloc(sizeof(int) * capacity);
        assert(_a);

        _top = 0;
        _capacity = capacity;
    }
    //拷贝构造
    Stack(const Stack& st)
    {
        _a = st._a;
        _top = st._top;
        _capacity = st._capacity;
    }
    //析构
    ~Stack()
    {
        free(_a);
        _a = nullptr;
        _top = _capacity;
    }
private:
    int* _a;
    int _top;
    int _capacity;
};

int main()
{
    //初始化栈
    Stack st1 ;
    //拷贝构造栈
    Stack st2(st1);
    return 0;
}

这个程序运行起来程序就崩溃了,为什么?仔细想想,我们这里使用的这种拷贝方式其实是有问题的,我们把st._a赋值给_a,st._top赋值给_top,st._capacity赋值给_capacity,此时的st1和st2都是指向同一块空间,这里是会出问题的!

出了作用域操作系统调用析构函数释放了st2的空间,紧接着又要释放st1的空间,由于两个对象指向的是同一块空间,就出现了连续释放两次同一块空间,这是非法的,我们这里的拷贝叫浅拷贝,所以像栈这类的需要使用深拷贝,不能使用浅拷贝。

浅拷贝:指向一块空间,修改数据会相互影响

这里也就说明了为什么一个对象赋给另一个对象需要使用拷贝构造函数,有些类需要特殊处理,而不是像C语言那样按照字节数拷贝,不然就会出现刚刚的问题

  • 若未显示定义,系统生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
class Date
{
public:
     Date(int year = 1900, int month = 1, int day = 1)
     {
         _year = year;
         _month = month;
         _day = day;
     }
private:
     int _year;
     int _month;
     int _day;
};
int main()
{
     Date d1;
     // 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
     Date d2(d1);
     return 0;
}

如下图:

C++之类和对象——中篇_第8张图片

这里跟构造和析构不同,内置类型的成员完成值拷贝(浅拷贝)。

那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
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);
}

我们会发现程序崩溃了,其实道理和上面是一样的,因为使用了浅拷贝:

C++之类和对象——中篇_第9张图片

 五. 赋值操作符重载

1. 运算符重载

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

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

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

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型或者枚举类型的操作数
  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
  • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
  • .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。(尤其注意是.*而不是*)
// 全局的operator==
class Date
{ 
public:
     Date(int year = 1900, 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;
}
void Test ()
{
     Date d1(2022, 9, 26);
     Date d2(2022, 9, 27);
     cout<<(d1 == d2)<
class Date
{ 
public:
     Date(int year = 1900, int month = 1, int day = 1)
     {
         _year = year;
         _month = month;
         _day = day;
     }
 
     // bool operator==(Date* this, const Date& d2)
     // 这里需要注意的是,左操作数是this指向的调用函数的对象
     bool operator==(const Date& d2)
     {
         return _year == d2._year;
         && _month == d2._month
         && _day == d2._day;
     }
private:
     int _year;
     int _month;
     int _day;
};
void Test ()
{
     Date d1(2022, 9, 26);
     Date d2(2022, 9, 27);
     cout<<(d1 == d2)<

2. 赋值运算符重载

class Date
{ 
public :
     Date(int year = 1900, int month = 1, int day = 1)
     {
         _year = year;
         _month = month;
         _day = day;
     }
 
     Date (const Date& d)
     {
         _year = d._year;
         _month = d._month;
         _day = d._day;
     }
 
     Date& operator=(const Date& d)
     {
         if(this != &d)
         {
             _year = d._year;
             _month = d._month;
             _day = d._day;
         }
     }
private:
     int _year ;
     int _month ;
     int _day ;
};

赋值运算符主要有四点:

  • 参数类型使用cosnst修饰,不然在传参时可能会涉及权限放大的情况
  • 返回值不能使用const修饰,因为类似连续赋值是会报错的,返回值需可写
  • 可以自己给自己赋值
  • 返回*this使用传引用返回,效率更高
  • 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝
class Date
{
public:
     Date(int year = 1900, int month = 1, int day = 1)
     {
         _year = year;
         _month = month;
         _day = day;
     }
private:
     int _year;
     int _month;
     int _day;
};
int main()
{
     Date d1;
     Date d2(2022,10, 1);
 
     // 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。
     d1 = d2;
     return 0;
}

那么编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

// 这里会发现下面的程序会崩溃掉,这里就需要我们以后讲的深拷贝去解决。
class String
{
public:
     String(const char* str = "")
     {
         _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("world");
 
     s1 = s2;
}

这里之前提到的情况一样,涉及到了动态内存申请,都是需要使用深拷贝的,浅拷贝会和之前的出现一样的问题。

默认拷贝构造与赋值运算符重载的问题:需要注意在下面这种情况采用的是默认拷贝函数而不是赋值运算符

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

	//传引用,减少拷贝,*this没有销毁,this销毁了
	return *this;
}
    //区分赋值与拷贝构造函数的情况
	Date d1(2022, 5, 19);
	Date d2 = d1;
	//调试查看发现这行代码其实是按这行代码的方式执行的Date d2(d1);

	Date d3;
	Date d4;
	d3 = d4;//这行代码才调用了重载赋值操作符函数

注:当类里和类外都同时存在同一个操作符的重载,如果没有指明调用哪里的,默认调用类里的操作符重载

六. const成员函数

1. const修饰类的成员函数

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

我们来看看下面的代码:

//这里的参数应该是Date* const this
void Print()
{
    cout << _year << "-" << _month << _day << endl;
}

//这里调用直接报错
void Func(const Date& d)
{
    d.Printf();
}

//调用d.Print()函数
void Test()
{
    Date d1(2022, 5, 18);
    d1.Print();
    Func(d1);
}

我们会发现就报错了:

C++之类和对象——中篇_第10张图片

 实际上,上面的代码应该被处理成以下这样:

C++之类和对象——中篇_第11张图片

这里就很明了,一个是Date* 类型传给Date* const 类型的,权限没变,一个是const Date*传给Date* const类型的,很明显,这里权限放大了,是非法的

所以这种时候,我们就应该使用到成员函数后面加const,因为this是隐含的,所以只能使用这种方法:

C++之类和对象——中篇_第12张图片

 此时就会变成:

C++之类和对象——中篇_第13张图片

一个是Date* 类型传给const Date* const 类型的,权限缩小,没问题,一个是const Date*传给const Date* const类型的,权限没变。此时发现编译运行通过了:

C++之类和对象——中篇_第14张图片

注意:将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。所以并不是什么时候都应该使用const,成员函数内需要进行修改的话就不能使用const修饰成员函数。在成员函数增加const也是为了让被const对象和普通对象都能调用该成员函数。成员函数中不修改成员变量的成员函数都可以加上const,普通对象和const对象都可以调用。

class Date
{ 
public :
     void Display ()
     {
         cout<<"Display ()" <

关于const成员函数和成员函数的调用:

  • const对象可以调用非const成员函数(权限放大)
  • 非const对象可以调用const成员函数(权限缩小)
  • const成员函数内可以调用其它的非const成员函数(权限放大)
  • 非const成员函数内可以调用其它的const成员函数(权限缩小)

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

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

(注意:第二个函数的两个const一个都不能少,一个是修饰的是*this,构成函数重载,参数被const修饰时使用该函数,一个是返回类型,返回的类型就是参数的类型,const Date*)

class Date
{ 
public :
     Date* operator&()
     {
         return this ;
     }
 
     const Date* operator&()const
     {
         return this ;
     }
private :
     int _year ; // 年
     int _month ; // 月
     int _day ; // 日
};

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

如下才需要自己写:

class Date
{
public:
    Date* operator&()
    {
        return nullptr;
    }

    const Date* operator&()const
    {
        return nullptr;
    }
private:
    int _year;
    int _month;
    int _day;
};

你可能感兴趣的:(#,C++,c++)