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
这些都可以增加编译器对代码进行优化的机会
我明白啦,那么~
这个&又是干嘛的?为什么要出现在这里?大佬们好像都这么写啊?
(程序姬再次素质三连)
这就是一个引用!
我们知道,拷贝构造函数的语法中必须有&:类名称(类名称 & 形参)
当我们用对象初始化对象的时候,编译器会自动调用拷贝构造函数
一旦调用拷贝构造函数,就相当于创建了一个新的对象
而引用是原对象的影之分身(引用和原对象共享内存)
使用 &
可以避免调用拷贝构造函数创建新对象,而且我们还可以通过引用修改原对象,简直不要太爽