C++类和对象梳理2

一、类的6个默认成员函数的引出

1.默认成员函数的概念:用户没有显示实现,编译器会生成的成员函数。

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在不写的情况下,都会自动生成下面6 个默认成员函数
二、类的默认函数详解:
1、构造函数:
class Date
{ 
public :
 void Print ()
 {
 cout <<_year<< "-" <<_month << "-"<< _day <<endl;
 }
 
 void InitDate(int year , int month , int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};
int main()
{
 Date d1, d2;
 d1.InitDate(2024,5,23);
 d2.InitDate(2024,6,23);
 d1.Print();
 d2.Print();
 return 0;
}

对于日期类,可以通过InitDate公有的方法给对象设置内容,但是每次创建对象都要调用该方法进行初始化太过于麻烦,因此构造函数便产生了。

(1)作用:

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

(2)特性:

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

特征如下:

1. 函数名与类名相同。

2. 无返回值。

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

4. 构造函数可以重载。
下面这段代码可以说明:
class Date
{ 
public :
 // 1.无参构造函数
 Date ()
 {}
 
 // 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 (2024, 5, 1); // 调用带参的构造函数
}

注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明,如下:声明了无参函数func,该函数返回类型为Date。

class Date
{ 
public :
 // 1.无参构造函数
 Date ()
 {}
 
 // 2.带参构造函数
 Date (int year, int month , int day )
 {
 _year = year ;
 _month = month ;
 _day = day ;
 }
private :
 int _year ;
 int _month ;
 int _day ;
};
Date func();//函数声明

还需要提醒的是:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义(不论定义多少,只要满足重载就行),编译器则不再生成,因此在显示定义构造函数时要考虑周全(避免出现只定义无参数的构造或只有有参数的构造,最好是定义全缺省的构造)。

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且只能有一个(否则会调用默认构造出现歧义)。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。(可以理解为可以不传参调用的就为默认构造)
​
class Date
{ 
public :
 // 1.无参构造函数
 Date ()
 {
    _year=2000;
    _month=1;
    _day=2;
 }
 
 // 2.带参构造函数
 Date (int year=2024, int month=5 , int day=1 )
 {
 _year = year ;
 _month = month ;
 _day = day ;
 }
private :
 int _year ;
 int _month ;
 int _day ;
};
void test()
{
    Date d1;//调用默认构造(会出调用现歧义)
}


​

上面的代码说明:无参数的默认构造和全缺省的默认构造虽然满足函数重载,但是会出现调用歧义。因此在应用的时候,一般都用全缺省的默认构造。

(3)特别说明:

当我们不自己写默认构造函数时,会调用编译器自动生成的默认构造,但是编译器自动生成的默认构造只对自定义类型初始化,对内置类型没有规定是否初始化(有些编译器会处理)!C++11对此打了补丁(可以对内置类型变量给缺省值)。下面代码可以说明

​
class Date
{ 
public :
 void print()
{
    cout<<_year<<" "<<_month<<" "<<_day<<endl;
}
private :
 int _year=1 ;//给缺省值
 int _month=1 ;
 int _day =1;
};


​

那么编译器自己生成的默认构造意义何在?这当然是有意义的,在一个类的成员变量都为自定义类型的时候,这个时候便会对自定义类型的成员变量进行初始化(调用它的默认构造,如果它的默认构造是编译器自动生成的,则里面的内置类型变量仍为随机值)。

(4)总结:

        1、自定义类型构造的尽头还是要对内置类型的成员进行构造,所以一般情况下,构造函数都要我们自己去实现。

        2、只有少数情况下不要自己写默认构造,比如类的成员变量都为自定义类型,且这些自定义类型的默认构造最好不是编译器自动生成的,而是自己显示定义的。

2、析构函数

(1)作用:
与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而
对象在销毁时会自动调用析构函数,完成类的一些资源清理工作(如对malloc、fopen、new等开出的空间进行free、fclose、delete)。
(2)特性:
1. 析构函数名是在类名前加上字符 ~
2. 无参数无返回值。(因此不支持函数重载)
3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时, 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;
};
int main()
{
    SeqList a;
    a.~SeqList();//显示调用析构
}
关于编译器自动生成的析构函数,是否会完成一些事情呢?会对自定义类型成员调用它的析构函数,对内置类型不做处理(与构造函数的处理规则类似)。如下代码a销毁的同时,调用它的析构,但是因为没有自己写析构,所以调用编译器自带的析构,而这个析构对内置类型不做处理,也就是说它对程序里malloc开出的空间没有处理,因此存在内存泄漏!
​
typedef int DataType;
class SeqList
{ 
public :
 SeqList (int capacity = 10)//构造
 {
 _pData = (DataType*)malloc(capacity * sizeof(DataType));
 assert(_pData);
 
 _size = 0;
 _capacity = capacity;
}
 
 
 
private :
 int* _pData ;
 size_t _size;
 size_t _capacity;
};
int main()
{
    SeqList a;
}//程序结束自动调用编译器自动产生的析构

​

(3)总结:

对于对象里开辟了空间资源的,需要自己写析构清理;对象里面没有额外开辟空间的,编译器自动生成的析构足够了。

3、拷贝构造

(1). 拷贝构造函数是构造函数的一个重载形式,用同类型的对象进行拷贝初始化

(2). 拷贝构造函数的 参数只有一个 必须使用引用传参 ,使用 传值方式会引发无穷递归调用
调用拷贝构造时与调用普通函数一样都要传参,当拷贝构造函数的参数不是引用类型,而是类类型的话,编译器便会对这个类类型的参数再次调用它的拷贝构造。(这里可以联系内置类型传行参进行理解。内置类型传行参实际传入的是:在函数内,又开辟了一个临时空间,将主函数中内置类型的值拷贝给子函数中的另一个变量)。因此,当拷贝构造的参数类型为类类型时,如此往复,每传值都调用一次拷贝构造,形成了无穷套娃,因此便引发了无穷递归。
class Date
{
public:
 Date(int year = 2024, int month = 5, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 Date(const Date& d)//防止对被拷贝对象的修改,所以加上const修饰更为稳妥
 {
 _year = d._year;
 _month = d._month;
 _day = d._day;
 }
private:
 int _year;
 int _month;
 int _day;
};
int main()
{
 Date d1;
// 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
 Date d2(d1);//Date d2=d1;这样也是拷贝构造,以赋值的形式。

 return 0;
}
(3) 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷 贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
但是系统默认的拷贝构造函数只能解决一些简单的拷贝(浅拷贝);当对象成员里含有指针时,形成地址拷贝,也就是说两个对象中的指针成员都指向同一块空间,因此当对其中一个对象中的空间操作时,另一个对象中的该空间也会改变,此时也会出现析构两次的情况,此时就需要深拷贝了。
深拷贝就是要开辟一个与原来大小相同的空间,然后将被拷贝对象的开辟空间的值赋值到要拷贝的开辟的空间。

(4)总结:

        1、如果没有管理资源,一般情况不需要写拷贝构造,系统默认生成的就够了。

        2、如果都是自定义类型成员,内置类型成员没有指向资源,默认生成的拷贝构造就可以。

        3、一般情况下,不需要写析构函数的,就不需要写拷贝构造。

        4、如果内部有指针或一些值指向资源,需要显示写拷贝构造(深拷贝),且要显示写析构。

4、赋值重载

(1)运算符重载:

C++为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数,也具有其返

回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

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

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

注意:

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

        2、重载操作符必须有一个类类型或者枚举类型的操作数

         3、 用于内置类型的操作符,其含义不能改变,例如:内置的整型+ ,不 能改变其含义
       
        4、 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
               操作符有一个默认的形参this,限定为第一个形参
        5、 .* 、 :: sizeof ?: . 5 个运算符不能重载。
引例:
​
class Date
{
public:
 Date(int year = 2024, int month = 5, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 
//private:
 int _year;
 int _month;
 int _day;
};
bool operator==(const Date& d1, const Date& d2)//全局定义,则类中的成员变量要是公有(这样才能访问),所以将private注释掉了
{
 return d1._year == d2._year;
 && d1._month == d2._month;
 && d1._day == d2.—_day;
}
int main()
{
 Date d1(2024,5,1);
 Date d2(2024,5,2);
 operator==(d1,d2);//显示调用
 d1==d2;//转换调用,编译器会转换成operator==(d1,d2);
 return 0;
}

​

当运算符重载定义在全局时,就破坏了封装性。要保证封装,有3种方式:1、提供这些成员的get和set函数(java中常见)2、在类内部声明友元(friend关键字)3、将此重载为成员函数。

​
​
class Date
{
public:
 Date(int year = 2024, int month = 5, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 bool operator==(const Date& d)
{
 return this->_year == d_year;//this可不写
 && this->_month == d._month;
 && this->_day == d._day;
}
 
//private:
 int _year;
 int _month;
 int _day;
};

int main()
{
 Date d1(2024,5,1);
 Date d2(2024,5,2);
 d2.operator==(d1);//显示调用
 d1==d2;//转换调用,编译器会转换成d1.operator==(d2);
 return 0;
}

​

​

如果全局和局部都定义了,那么优先调用类里面的函数。

(2)赋值运算符重载:

赋值重载与拷贝构造是有区别的。拷贝构造是用一个已初始化的对象初始化一个没有初始化的对象;而赋值重载是将一个已初始化对象的值拷贝给另外一个已初始化的对象。

class Date
{
public:
 Date(int year = 2024, int month = 5, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 Date(const Date& d)
 {
 _year = d._year;
 _month = d._month;
 _day = d._day;
 }
void operator=(const Date& d)//赋值重载;这样写完备吗?d1=d2=d3;这样的赋值还支持吗?
 {
  if(this != &d)
  {
  _year = d._year;
  _month = d._month;
  _day = d._day;
  }
 }
private:
 int _year;
 int _month;
 int _day;
};
int main()
{
 Date d1(2024,5,1);
 Date d2(2024,5,2);
 Date d3=d1;//拷贝构造
 d1=d2;//赋值重载
return 0;
}

上面的赋值重载如果以没有返回值的形式写,那么在连续赋值的操作情况下就行不通了。因此可以如下面这个代码进行改进:

Date operator=(const Date& d)//传值返回
{
  if(this != &d)
  {
  _year = d._year;
  _month = d._month;
  _day = d._day;
  }
    return *this;
}
Date& operator=(const Date& d)//传引用返回
{
  if(this != &d)
  {
  _year = d._year;
  _month = d._month;
  _day = d._day;
  }
    return *this;
}

传值返回,返回的是对象的拷贝;传引用返回,返回的是对象的别名(实际还是指向同样的空间)

传引用返回的效率要高一些,但是要确保返回的对象此时还没有被销毁。这里总结一下:

1、返回对象生命周期到了,会析构,传值返回。

2、返回对象生命周期没到,不会析构,传引用返回。

所以综上,在d1=d2时,调用赋值重载,此时返回的对象是*this也就是d1,而d1此时的生命周期还没有截止,所以不会被销毁,所以这里传引用会更好,效率会更高(减少拷贝)。

(3)cin和cout打印自定义类型

c++的流插入和流提取默认支持内置类型,但是他们却不默认支持自定义类型。这是因为cout和cin是全局对象,分别叫ostream和istream。

int i=0;
cout<<i;
cout.operator<<(i);//两者等价

因此,在这里就知道了为什么cout/cin能自动识别类型,这是因为这些流插入(输出)重载构成函数重载。

​
class Date
{
public:
 Date(int year = 2024, int month = 5, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 Date(const Date& d)
 {
 _year = d._year;
 _month = d._month;
 _day = d._day;
 }
void operator<<(ostream& out); 
private:
 int _year;
 int _month;
 int _day;
};
void Date::operator<<(ostream& out)
{
    out<<_year<<" "<<_month<<" "<<_day<<endl;
}
int main()
{
 Date d1(2024,5,1);
 d1.operator(cout);
 d1<<cout;//两者等价
return 0;
}

​

这里可能有人会有疑问:平时写输出都是cout在前面,为什么这里在后面?其实原因很简单。这是因为运算符重载中参数顺序和操作数顺序是一致的;而d1传的参数是隐式的this指针,它已经占有了最左边的位置。因此,operator想重载为成员函数是可以的,但是不符合正常逻辑,但当重载为全局的时候就符合正常逻辑了,但此时要访问对象的私有成员就不行了,要把对象的私有成员变为公有(public)但这是就不满足封装了。

void operator<<(ostream& out,const Date& d)//定义为全局
{
    out<<d._year<<" "<<d._month<<" "<<d._day<<endl;
}
cout<<d;//此时可以这样写

此外,如果想保持一定的封装特性,我们可以在全局定义,在类内部声明友元。

5、const成员函数

const修饰的类成员函数称之为const成员函数const修饰类成员函数,实际修饰该成员函数

含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

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

int main()
{
const Date d1(2024,5,30);
d1.print();//会报错,因为此时为权限的放大
return 0;
}​

​

上述的代码中,因为d1被const修饰,所以d1只能被只读,而成员函数print里的隐式参数this的类型为Date* 可读可写,所以是权限的放大。那么有什么解决方法呢?因为this指针是隐式的,所以直接修改它的类型行不通。这里就要用到const成员函数了。操作如下:

​
​
​
class Date
{ 
public :
Date(int year = 2024, int month = 5, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 void print()const    //const 成员函数
{
    cout<<_year<<" "<<_month<<" "<<_day<<endl;
}
private :
 int _year=1 ;
 int _month=1 ;
 int _day =1;
};

int main()
{
const Date d1(2024,5,30);
d1.print();//会报错,因为此时为权限的放大
return 0;
}​

​

​

此时在函数括号后面加一个const就可以了,这里的const是修饰参数(隐式的*this对象)。

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

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

class Date
{ 
public :
 Date* operator&()
 {
 return this ;
 }
 
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
  • 22
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++是一种面向对象的编程语言类和对象C++中的重要概念。下面是关于类和对象的知识梳理: 1. 类是一种用户自定义的数据类型,它可以包含数据成员和成员函数。数据成员可以是各种类型的变量,成员函数可以访问和操作这些数据成员。 2. 类的定义包括类名、数据成员和成员函数。类的数据成员和成员函数可以分为公有的和私有的两种。公有的成员可以被类外的函数访问,私有的成员只能被类内的成员函数访问。 3. 对象是类的一个实例,它可以使用类中定义的成员函数来操作数据成员。对象的创建需要使用类名和构造函数进行初始化。 4. 构造函数是用于初始化对象的特殊成员函数,它与类名相同,没有返回类型,并且可以有参数。构造函数可以被重载,使得对象可以以不同的方式进行初始化。 5. 析构函数是用于销毁对象的特殊成员函数,它与类名相同,前面加上“~”符号。析构函数在对象被销毁时自动调用,用于清理对象占用的资源。 6. 成员函数可以是普通的成员函数或静态成员函数。普通的成员函数可以访问对象的数据成员和其他成员函数,静态成员函数不能访问对象的数据成员,只能访问静态数据成员和其他静态成员函数。 7. 类可以继承其他类,并且可以使用访问控制修饰符来控制继承的范围。继承的类称为派生类,被继承的类称为基类。 8. 多态是面向对象编程的重要概念之一,它可以使得不同类型的对象调用相同的函数产生不同的行为。C++中通过虚函数实现多态,虚函数是在基类中声明为虚函数的成员函数,派生类可以重新定义这个函数。 9. 友元函数是一个非成员函数,但可以访问类的私有成员。友元函数必须在类中声明为友元函数。 以上就是C++类和对象的知识梳理,希望对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值