C++类&&对象的二次深入研究

const对象 & const成员函数

一些对象是支持修改的,而另外一些是坚决不允许修改的。我们可以使用关键字const来指定对象为不可修改,这样任何试图修改对象的操作都将导致编译错误。

将变量和对象声明为const可以提高性能,编译器可以对常量提供某些针对const关键字的特殊优化

class Time{
public:
    Time(int h,int m,int s):hour(h),minute(m),second(s);
private:
    int hour,minute,second;
};
...
const Time noon(12,0,0);

在上面的栗子中,我们申请了一个const对象:noon
那么const对象有什么特性呢?
const对象只能调用该类的const member function ( 常成员函数 )

对象类型 成员函数类型 是否可以成功调用
const const
const non-const -
non-const const
non-const non-const

那么什么是const成员函数 ( 常成员函数 ) 呢?

class Time{
public:
    Time(int h,int m,int s):hour(h),minute(m),second(s) {}
    void setHour(int);
    void setMinute(int);
    void setSecond(int);
    int getHour() const;
    int getMinute() const;
    int getSecond() const;
private:
    int hour,minute,second;
};

getHour(),getMinute(),getSecond()是常成员函数
我们凭借敏锐的观察力可以发现,常成员函数在定义的时候,函数签名后会紧跟一个const

注意

  • 常成员函数的基本特征有:
    • 显式地声明为const
    • 不能修改本对象(的数据成员),但是可以修改非本对象的数据成员
    • 不能调用本对象的其他non-const成员函数,但是可以调用非本对象的non-const成员函数
  • 构造函数和析构函数不能声明为const
  • 对象的常量特性体现在初始化(构造)后,析构之前
  • 建议将所有不更改对象成员的函数均声明为const成员函数,提高程序效率

构造函数初始化列表

初学者在编写构造函数的码风:

Time::Time(int h,int m,int s) {
    hour=h;
    minute=m;
    second=s; 
}

那么现在可以告诉你啦~

这样的写法叫做赋值,不是真正的初始化

最正宗的初始化方法是构造函数初始化列表

Time(int h,int m,int s):hour(h),minute(m),second(s) {}
//括号外是private数据成员,括号内是其对应的初始值
//不要忘了后面的{}

所有的类成员都可以用构造函数初始化列表进行初始化,但是有一些特殊情况只能使用初始化列表进行初始化:

  • const data member 常数据成员**(特例:const static integer)**
  • reference data member 引用类型的数据成员
  • member objects 数据成员是其他类(而且未提供缺省构造函数)的对象
  • base class 继承类的基类
//Increment.h

class Increment{
public:
    Increment(const char*,int =0,int =1);
    //带默认参数的缺省构造函数
    void addIncrement() {
        count+=increment;
    }
    void print() const;
    //常成员函数
private:
    int count;
    const int increment;
    //常数据成员,只能使用初始化列表进行初始化
    int &refCount;
    //引用类型的数据成员,只能使用初始化列表进行初始化
    const char* _name;
    //常量指针,指向char类型常量,字符串不支持修改
};
//Increment.cpp

#include<iostream>
using namespace std;

Increment::Increment(const char* name,int c,int i) : _name(&name),count(c),increment(i),refCount(count) 
//注意_name的初始化,是对指针类型变量的初始化,所以要用取地址符&name
//引用类型的数据成员初始化 refCount(count) 
{
    cout<<"Now count="<<count<<",increment="<<increment<<",refCount="<<refCount<<endl;
    refCount=99;
}

void Increment::print() const {
    cout<<"count="<<count<<",increment="<<increment<<endl;
}
//Test.cpp

Increment member(10,5);
member.print();

// Output
Now count=10,increment=5,refCount=10
count=99,increment=5

组合:对象作为类的成员

就像标题一样,这一部分我们要尝试将对象作为类的成员

class Date{
public:
    Date(int =2020,int =1,int =1);
private:
    int year,month,day;
};

class Time{
public:
    Time(int =0,int =0,int =0);
private:
    int hour,minute,second;
    Date D;   //成员对象
};

第一个问题:如何初始化?
普通的数据成员我们已经会用构造函数初始化列表解决了,规范又拉风

但是成员对象怎么初始化呢?

(为了方面描述,下面我们就利用Time和Date来描述这两个类之间的关系)

  • 若Date有缺省构造函数(未自定义或有默认实参的构造函数),则允许在Time的构造函数初始化列表中不对成员对象Date进行初始化,随后通过Time的public类set函数对成员对象进行赋值
    因为在申请成员对象的时候会隐式调用成员对象的缺省构造函数,所以没有特别的初始化也可以成功
  • 若Date无缺省构造函数,要在构造函数中初始化成员对象,就必须使用初始化列表完成成员对象的初始化
    这时调用Date的拷贝构造函数
  • 若成员对象没有显式通过成员初始化列表初始化,则自动隐式调用其缺省构造函数

对象创建的步骤

  • 分配内存,数据成员初始化
    • 按照类定义的顺序,为数据成员分配内存空间,并初始化每个数据成员
    • 如果初始化列表中未指定初始化,则使用系统提供的缺省构造函数进行初始化
  • 执行constructor函数体内的语句
构造函数初始化列表

必须使用的情况:

  • const data member 常数据成员 (特例:const static integer)
  • member object without default constructor 无缺省构造函数的成员对象
  • reference data member 引用类型的数据成员

不能使用的情况:

  • 常量数组
    解决方法:
    const static int array[];
    const int 类名::array[]={1,2,3};

建议:确保初始化列表中成员列出的顺序和成员在类内的声明顺序一致

成员对象的构造和析构顺序

  • 成员对象的构造先于宿主对象
  • 成员对象按照类定义中的声明顺序构造
  • 成员对象的析构后于宿主对象
#include<iostream>
using namespace std;

class Date{
public:
	Date(int y=2020,int m=3,int d=11)
	    :year(y),month(m),day(d) {cout<<"Create Date\n";}
	//有默认参数的缺省构造函数
	~Date() {cout<<"Destroy Date\n";}
	Date(Date &d)   //拷贝构造函数
	{
        setData(d.year,d.month,d.day);
        cout<<"Copy Constructor is called.\n";
    }
    void setData(int y,int m,int d) {year=y,month=m,day=d;}
	void print() const{
        cout<<"Date:"<<year<<"/"<<month<<"/"<<day<<" ";
	}
private:
    int year,month,day;
};

class Time{
public:
	Time(int h,int m,int s,Date &day)
		:hour(h),minute(m),second(s),D(day) {cout<<"Create Time\n";}
    //初始化列表中D(day)调用的是拷贝构造函数
	~Time() {cout<<"Destroy Time\n";}
	void print() const {
	    D.print();
		cout<<hour<<":"<<minute<<":"<<second<<endl;
	}
private:
    int hour,minute,second;
    Date D;   //成员对象
};

int main()
{
	Date Today;
	Time Now(22,57,45,Today);
	Now.print();
	return 0;
}
//Output
Create Date
Copy Constructor is called.
Create Time
2020/3/11 22:57:45
Destroy Time
Destroy Date

友元函数 & 友元类

为什么开发C++的前辈总是想搞事情:如果我想要让类外部(不属于该类)的函数或类,能够访问该类的非公有成员,怎么办呢?

Friend Function(友元函数) & Friend Class(友元类 )

使用方法说简单也很简单,只要在非公有成员所在类中用friend关键字声明即可
被声明的内容可以在该类class大括号之外的区域,访问该类的非公有成员了
(注意我的表述,认真理解参悟friend关键字的玄妙之处)

class Count{
    friend void setX(Count &,int);
public:
    Count():x(0) {}
    void print() const {cout<<x<<endl;}
private:
    int x;
};

void setX(Count &c,int value) {c.x=value;}

//---

int main() 
{
    Count counter;
    counter.print();
    
    setX(counter,3);
    counter.print();

    return 0 ;
}

//Output
0
3
class ClassOne {
    friend class ClassTwo;
    int x,y;
};

class ClassTwo{
public:
   void setX(ClassOne &one,int value) {one.x=value;}
   void setY(ClassOne &one,int value) {one.y=value;}
   void print(ClassOne &one) {
       cout<<"one.x="<<one.x<<",one.y="<<one.y<<endl;
   }
};

//---

int main() 
{
    ClassOne one;
    ClassTwo two;
    two.setX(one,3);
    two.setY(one,9);
    two.print(one);

    return 0 ;
}

//Output
one.x=3,one.y=9

友元函数 & 友元类的存在意义:提高性能
典型应用:运算符重载


this指针

之前我们简单了解了this指针
this指针的原型:<类名> *const this
特例:static member function
在这里插入图片描述
我们既可以隐式调用this指针,也可以显式调用:

this->x
(*this).x

然而,this指针还有一个更玄妙的用法:级联函数调用
看一下这个栗子:

//Time.h
class Time {
public:
    Time(int =0,int =0,int =0);
    Time &setTime1(int,int,int);
    Time setTime2(int,int,int);
    void print() const;
private:
    int hour,minute,second;
};

//Time.cpp
Time &Time::setTime1(int h,int m,int s) {
    hour=h;
    minute=m;
    second=s;
    return *this;
}
Time Time::setTime2(int h,int m,int s) {
    hour=h;
    minute=m;
    second=s;
    return *this;
}
void Time::print() const {
    cout<<hour<<":"<<minute<<":"<<second<<endl;
}

//TimeTest.cpp
int main() 
{
    Time t1,t2;
    t1.setTime1(20,20,20)
      .setTime1(18,20,22);
    t2.setTime2(20,20,20)
      .setTime2(18,20,22);
    t1.print();
    t2.print();
    return 0;
}

//Output
18 20 22
20 20 20

简单解释一下:
setTime1是Time &类型的函数,返回值是一个引用
引用是原对象的影之分身,两者共享内存,所以两次调用setTime1,修改的都是本对象*this的数据成员

Time &setTime1(int h,int m,int s);
t1.setTime1(20,20,20)
  .setTime1(18,20,22);
// equal to
Time &tmp=t1.setTime1(20,20,20);
tmp.setTime1(18,20,22);

setTime2是Time类型的函数,返回值是一个Time类的对象
tmp享有独立内存,所以第二次调用setTime2,修改的都是tmp的数据成员

Time setTime2(int h,int m,int s);
t2.setTime2(20,20,20)
  .setTime2(18,20,22);
//equal to
Time tmp=t2.setTime2(20,20,20);
tmp.setTime2(18,20,22);

有感而发关于const和&的思考

我要把这些杂七杂八的思考都藏到最后(反正blog写出来没有人看QwQ)

我是怎么想到这个问题的呢?
深夜,我深情凝望着我呕心沥血写出的高精度加减(类)代码
小小的脑袋里装满了大大的问号:

HugeInteger add(const HugeInteger &); 
这个const的干嘛的?为什么要出现在这里?大佬们好像都这么写啊?

(程序姬素质三连)

在函数的参数中使用const,可以让编译器知道在函数调用过程中,不会修改这个参数,从而可以提供给编译器更多的优化机会
然而const属性加入后,这个参数就会变成常量,如果要将这个参数传到其他函数中,就必须在形参列表中加入const属性

同理,我们有时会标明某个成员函数是const,表示这个成员函数不会修改这个类对象的任何数据

对于一个函数,如果某个指针参数指向的内容不会被修改,就应该加上const属性

此外,对于常数变量,同样也要在能够添加const时就添加const
这些都可以增加编译器对代码进行优化的机会

我明白啦,那么~

这个&又是干嘛的?为什么要出现在这里?大佬们好像都这么写啊?

(程序姬再次素质三连)

这就是一个引用!
我们知道,拷贝构造函数的语法中必须有&:类名称(类名称 & 形参)
当我们用对象初始化对象的时候,编译器会自动调用拷贝构造函数
一旦调用拷贝构造函数,就相当于创建了一个新的对象
而引用是原对象的影之分身(引用和原对象共享内存)
使用 & 可以避免调用拷贝构造函数创建新对象,而且我们还可以通过引用修改原对象,简直不要太爽

发布了964 篇原创文章 · 获赞 235 · 访问量 34万+

猜你喜欢

转载自blog.csdn.net/wu_tongtong/article/details/104762074
今日推荐