前言: 经过前几篇博客的学习,相信对于类和对象大家有了比较深刻的认识,并且能够简单的搭建起来。本篇博客,会讲述一些比较琐碎的知识,如,const成员、取地址及const取地址操作符重载、初始化列表、explicit关键字等等,这些不是主要矛盾点,虽然琐碎,但学起来是比较轻松的,咱们的重头戏还是前几篇博客,希望有遗忘,或者还没有了解的同志,可以看下本人前几篇博客:类和对象之拷贝构造和运算符重载、类和对象之构造函数与析构函数、类和对象入门、内联函数、auto关键字、范围for、引用超详解、命名空间、缺省参数及函数重。希望大家有所收获!
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
void Print() const //实际上编译器会处理成void Print(const Date* this)
{
cout << _hour << ":" << _minute << ":" << _second << endl;
}
补充知识:
const Time* p1;//const修饰的是 p1指向的对象
Time const* p2;//const修饰的是 p2指向的对象
Date* const p3;//const修饰的是 p3指针本身
注意:这里p1、p2中的const均修饰的是指针指向的对象,而p3中的const是修饰的是指针本身,这需要格外注意。
class Time
{
public:
//void Print()
void Print() const//因此,print成员函数被const修饰一下就解决下述问题。实际上编译器会处理成void Print(const Date* this)
{
cout << _hour << ":" << _minute << ":" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
const Time t1;
t1.Print();//由于Print成员函数是非const所以这里会报错
return 0;
}
//下述函数均是类成员函数
void f1()//void f1(Date* this)
{
f2();//this->f2(this)
}
void f2()
{}
//void f3() //由于f3是一个普通的类成员函数,即函数体内是可以对this指向的内容做修改的
//{} //但是你f4调用的时候是const的是不能修改的,这就涉及到了权限放大的问题了,所以想调用必须使f3成为const成员函数
void f3() const
{}
void f4() const//void f4(const Date* this)
{
f3();//this->f3(this)
}
总结:
类别 | 语法 | 特性与规则 | 使用场景 |
---|---|---|---|
const 成员变量 | const dataType varName; |
| 定义对象生命周期内不变的常量属性(如圆周率、固定配置项) |
const 成员函数 | returnType func() const; |
| 提供对象只读接口,保证不修改对象状态(如 getter、打印函数) |
mutable 成员变量 | mutable dataType varName; |
| 需要在 const 方法中修改的辅助状态量(如调试计数器、缓存标志) |
const 对象 | const ClassName obj; |
| 定义只读对象,防止意外修改(如配置对象、共享数据) |
指向 const 的 this | const ClassName* this |
| 自动应用于 const 成员函数,保证函数内不修改对象状态 |
const 重载 | 同函数名提供 const 和非 const 版本 |
| 根据对象常量性提供不同实现(如返回 const/non-const 引用) |
思考下面的几个问题: 1️⃣ const对象可以调用非const成员函数吗? 2️⃣ 非const对象可以调用const成员函数吗? 3️⃣ const成员函数内可以调用其它的非const成员函数吗? 4️⃣ 非const成员函数内可以调用其它的const成员函数吗? 答: 1️⃣ 显然是不能的,对象是只读的,但是非const成员函数函数体内是默认可以修改this指向的内容的,所以是不行的。 2️⃣ 显然是可以的,非const对象是可读可写的,那调用一个只读的成员函数,当然可行。 3️⃣ 显然是不可以的,上述已经给了例子,仍然是只读的给了可读可写的。 4️⃣ 显然是可以的,可读可写给了只读当然可行。
前面我们已经介绍了四个默认成员函数,其实在认识默认成员函数的开篇我们就已经介绍了事实上的默认成员函数是有六个,现在我们就介绍另外这两个,这两个没啥大的作用仅作了解即可。
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 1)//全缺省参数
{
_hour = hour;
_minute = minute;
_second = second;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
const Time t1;
Time t2;
cout << &t1 << endl;
cout << &t2 << endl;
return 0;
}
上述&调用的就是编译器自动生成的,大家需要注意,我特意写了一个const和非const的,下面我们来自己定义:
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 1)//全缺省参数
{
_hour = hour;
_minute = minute;
_second = second;
}
Time* operator&()
{
return this;
}
const Time* operator&() const
{
return this;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
const Time t1;
Time t2;
cout << &t1 << endl;
cout << &t2 << endl;
return 0;
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需 要重载,比如想让别人获取到指定的内容! 就像我们不希望别人能取到某些类的地址就可以这样做:
Time* operator&()
{
return nullptr;
}
const Time* operator&() const
{
return nullptr;
}
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。 下面来认识一下:
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
之前我们都是在函数体内赋值,这里是在函数体外,相信我们不约而同的有下述几个问题:
1️⃣ 为什么要有初始化列表,直接在函数体内进行初始化不就行了? 核心原因:初始化列表是成员变量和基类子对象 真正初始化 的地方,而构造函数体内的“=”操作实际上是 赋值 操作。 类实例化时构造顺序:
{}
大括号内的代码。2️⃣ 为什么单单有几个特殊的成员变量需要在初始化列表当中初始化?
这些特殊成员变量之所以强制必须在初始化列表中初始化,根本原因在于它们的语义和特性决定了它们必须在对象构造的早期阶段(即初始化列表阶段)就确定其初始状态,并且之后不能再被改变。在构造函数体内进行“赋值”操作要么不符合它们的语义,要么在语法上就是非法的。主要有三类:
成员类型核心特性/约束为什么不能在构造函数体内“初始化”为什么必须在初始化列表初始化const
成员值不可变构造函数体内是赋值操作,违反 const
语义 (编译错)构造时赋予初始值,之后不可变引用 (**&
**) 成员必须绑定且绑定后不可更改所指对象构造函数体内无法进行初始绑定 (编译错)构造时直接绑定到目标对象无默认构造的类成员编译器无法自动调用默认构造函数进行初始化不显式初始化,编译器尝试调默认构造会失败 (编译错)必须显式指定调用哪个带参构造函数来构造 简单记忆: 如果一个成员 必须在出生时就确定好且之后不能改 ( const
, 引用),或者 编译器不知道如何让它出生 (无默认构造函数的类成员),那么你就 必须 在初始化列表(对象出生的关键阶段)给它安排好。构造函数体(出生后)对这些成员来说已经太晚了或者操作不合法
//三种必须在初始化列表进行初始化定义的变量,这也是初始化列表存在的意义
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)//这里会报错的原因就是对于下面三个成员变量的初始化定义是发生在函数体执行之前(这也是为什么单独它们三个就必须在初始化列表当中)
: _aobj(1)//即只能是在初始化列表当中进行初始化定义
,_ref(ref)
,_n(a)
{
}
private:
A _aobj;
// 没有默认构造函数
int& _ref;
// 引用
const int _n; // const
};
解决【C++基础篇】学习C++就看这篇--->类和对象之构造函数与析构函数 ,中的遗留问题:为什么我们自定义的构造函数内为涉及成员对象,但在函数实例化时仍然调用了成员对象的构造函数? 相信听了上述的讲解大家已经清楚了,就是说成员对象的初始化是在主类的构造函数函数体执行之前,已经执行过了因而导致了你虽未涉及但我仍然调用的现象,所以我们也不能说这样的一个现象,把它说成是默认构造函数的唯一的作用,但也可以这样说吧,因为默认构造函数似乎也没啥其它作用了。证明如下:
class Time
{
public:
Time(int hour = 0)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
//Date(int day)//这里为了验证就是自定义成员类的初始化构造是发生在函数体执行之前,无论你有没有提到它都会是自动调用的
//{} //这也就证实了上篇博客所讲述的自定义类调用它的拷贝构造并不是编译器自己生成的构造函数里面的函数体内容去调用的
//不过也可以认为是它生成的构造函数去调的吧,实际是初始化列表进行初始化的
Date(int day)
{
Time t(1);
_t = t;//如果非要在函数体内进行初始化只能是这样进行,先实例化一个t再2赋值给_t,因为此时_t已经完成了定义
}
private:
int _day;
Time _t;
};
int main()
{
Date d(1);
}
看一个小题结束初始化列表这部分内容:
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)//一般建议定义与声明一致
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();//答案是_a1是具体值,_a2是随机值。
//成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后
//次序无关
}
这在上面的内容中也提到了,成员变量初始化是根据声明顺序进行的。
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值 的构造函数,还具有类型转换的作用。
int main()
{
Time t1(11, 5, 59);//构造
Time t2 = t1;//拷贝构造
Time t3 = 5;//涉及隐式类型转换,编译器后台会先Time temp(5),再Time t3(temp),但是临时变量不是必须的,看编译器的优化
//这里就会直接去调用构造
//Time t3& = 5加入引用就会报错,因为临时变量具有常性必须加入const
return 0;
}
补充知识:
上述内容我们见到的不止一次了,这里再作补充以加深印象,上述内容就类似于:
int i = 1;
double& d = i;
你会发现上述语句二是编不过去的,就是因为i赋值给d之前会先生成一个临时变量double类型的空间存储这个值,我们知道临时变量具有常性,所以可读可写的引用肯定是编不过,因此只能加const:
const double& d = i;
class Time
{
public:
explicit Time(int hour = 0, int minute = 0, int second = 1)//全缺省参数
{
_hour = hour;
_minute = minute;
_second = second;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
void Print()
{
cout << _hour << ":" << _minute << ":" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Time t1(11, 5, 59);//构造
Time t2 = t1;//拷贝构造
Time t3 = 5;//涉及隐式类型转换,编译器后台会先Time temp(5),再Time t3(temp),但是临时变量不是必须的,看编译器的优化
//这里就会直接去调用构造
//如果不想让上述情况发生,在构造函数前加explicit关键字就可以解决,可以发现就会报错
return 0;
}
补充知识:
int main()
{
Time t1(11, 5, 59);//构造
Time t2 = t1;//拷贝构造
Time t3 = { 2020, 5, 20 };//对于多参数,是这样定义的,当然和上面一样,如果不希望这样的情况发生加explicit关键字
//这是C++11里面新添的,C++98不支持
return 0;
}
总结一下: 具有隐式类型转换的代码可读性不是很好,所以用explicit修饰构造函数,将会禁止构造函数的隐式转换。
本篇博客我们了解了const 成员函数使用 const 修饰,其隐含 this 指针为 const 类型,在函数体内不能修改类成员变量且只能调用其他 const 成员函数。取地址操作符重载通常无需手动进行,但在特殊情况下可自定义。初始化列表用于在构造函数中初始化成员变量,对于 const 成员、引用成员及无默认构造函数的类成员等必须在初始化列表中初始化。explicit 关键字可防止构造函数发生隐式类型转换。以上内容总结了 C++ 中相关知识点的语法、特性、规则及使用场景等方面的内容。希望大家有所收获!