const成员
const
成员是指在类中声明的成员(变量或函数),其值或行为在对象的整个生命周期中都是不可修改的。这有助于确保对象的某些属性或行为不会被意外地更改,从而提高代码的可靠性和可维护性。
C++中有三种使用 const
的情况:const
成员变量、const
成员函数、const
对象。
const成员变量
这是类中被声明为 const
的成员变量。一旦对象被创建,这些变量的值就不能被修改。const
成员变量必须在类的构造函数的成员初始化列表中进行初始化。
class MyClass {
public:
MyClass(int value)
: myConstValue(value) {}
int getValue() const {
return myConstValue;
}
private:
const int myConstValue;
};
const成员函数
const
成员函数承诺不会修改对象的任何非 mutable
成员变量,并且可以在const
对象上调用。这允许在const
对象上执行只读操作。const
修饰类成员函数,实际修饰该成员函数隐含的this
指针,表明在该成员函数中不能对类的任何成员进行修改。
class MyClass {
public:
int getValue() const { return myValue; } // 不修改成员变量
void setValue(int value) const { myValue = value; } // err 不能修改成员变量
private:
int myValue;
};
需要注意的是,mutable
关键字可以用于修饰成员变量,即使在 const
成员函数内,这些被标记为 mutable
的变量仍然可以被修改。
class MyClass {
public:
int getValue() const { return myValue; } // 不修改成员变量
void setValue(int value) const { myValue = value; } //即使是const成员函数,也可以修改mutable成员变量
private:
mutable int myValue;
};
const对象
const
对象不能调用非const
成员函数。const
对象不能修改其成员变量的值(除非这些成员变量被声明为mutable
)。
class MyClass {
public:
void setValue(int v) {
value = v;
}
private:
int value;
};
int main() {
const MyClass obj;
// obj.setValue(10); // 错误: const对象不能调用非const成员函数
return 0;
}
const成员的权限问题
问题:
1.const对象可以调用非const成员函数吗? 答:不可以。因为const对象的成员函数是const的,而非const成员函数不是const的,两者不匹配。如果强制调用,会导致编译错误。
2.非const对象可以调用const成员函数吗? 答:可以。因为非const对象的成员函数既可以是const的,也可以是非const的,两者匹配。
3.const成员函数内可以调用其他的非const成员函数吗? 答:不可以。因为const成员函数不能修改对象的状态,而非const成员函数可能会修改对象的状态,这样会导致编译错误。
4.非cosnt成员函数内可以调用其他的cosnt成员函数吗? 答:可以。因为非cosnt成员函数既可以是cosnt的,也可以是非cosnt的,两者匹配。
static成员
静态成员是类的成员,它们与类本身相关,而不是与类的实例(对象)相关。静态成员在所有类的实例之间共享,并且可以通过类名访问,而不需要创建类的实例。C++中有两种类型的静态成员:静态数据成员和静态成员函数。
静态成员变量
这是类的成员变量,它在类的所有实例之间共享。静态数据成员在类的声明中被声明为static
,但是它们需要在类外部进行定义和初始化。通常,这是为了确保类的所有实例共享同一个数据。
#include <iostream>
using namespace std;
class MyClass {
public:
void setStaticVar(const int &x) {
staticVar = x;
}
int getStaticVar() {
return staticVar;
}
private:
static int staticVar;// 静态数据成员的声明
};
int MyClass::staticVar = 0;// 静态数据成员的定义和初始化
int main() {
MyClass obj1;
MyClass obj2;
//obj1.staticVar = 5; //err,对private成员访问
obj1.setStaticVar(5);
std::cout << obj2.getStaticVar();// 输出为 0,因为静态数据成员在所有实例之间共享
return 0;
}
注意: 这里静态成员变量staticVar虽然是私有,但是我们在类外突破类域直接对其进行了访问。这是一个特例,不受访问限定符的限制,否则就没办法对静态成员变量进行定义和初始化了。但是只限于对静态成员的定义和初始化。
静态成员函数
静态成员函数与类相关联,但不操作类的实例数据,因此它们没有隐式的this
指针。静态成员函数可以通过类名直接调用,无需创建类的对象。静态成员函数没有隐藏的this指针,不能访问任何非静态成员。通常,它们用于执行与类相关的操作,不需要访问实例特定的数据。
class Test {
public:
static void Fun() {
cout << _a << endl;//error不能访问非静态成员
cout << _n << endl;//correct
}
private:
int _a; //非静态成员
static int _n;//静态成员
};
小贴士:含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量。(普通成员函数也可以访问静态成员)
访问静态成员变量的方法
当静态成员变量为公有时,有以下几种访问方式:
class Test {
public:
static int _n;//公有
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main() {
Test test;
cout << test._n << endl; //1.通过类对象突破类域进行访问
cout << Test()._n << endl;//3.通过匿名对象突破类域进行访问
cout << Test::_n << endl; //2.通过类名突破类域进行访问
return 0;
}
当静态成员变量为私有时,有以下几种访问方式:
class Test {
public:
static int GetN() {
return _n;
}
private:
static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main() {
Test test;
cout << test.GetN() << endl; //1.通过对象调用成员函数进行访问
cout << Test().GetN() << endl;//2.通过匿名对象调用成员函数进行访问
cout << Test::GetN() << endl; //3.通过类名调用静态成员函数进行访问
return 0;
}
静态成员和类的普通成员一样,也有public、private和protected这三种访问级别,所以当静态成员变量设置为private时,尽管我们突破了类域,也不能对其进行访问。
总结:
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
注意区分两个问题:
1、静态成员函数可以调用非静态成员函数吗?
不可以。静态成员函数不能直接调用非静态成员函数。静态成员函数属于类本身,而不属于类的任何特定实例。因此,它们没有this指针来引用当前类的实例。非静态成员函数是与类的特定实例绑定的,因为它们可能访问或修改实例成员变量,所以必须通过一个类的实例来调用。
2、非静态成员函数可以调用静态成员函数吗?
可以。非静态成员函数可以直接调用静态成员函数。因为静态成员函数属于类本身,不需要类的实例就可以访问。由于非静态成员函数是在类的实例上下文中执行的,它们自然可以访问与类相关的所有静态成员,包括静态成员函数。
友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
友元函数
友元函数是一种特殊类型的函数,它被允许访问类的私有成员,尽管这些函数不是类的成员。友元函数通常在类的声明中通过friend
关键字进行声明,并且可以访问声明它为友元的类的私有和受保护成员。这种机制提供了对封装性的一定程度的破坏,但有时候它是必要的,例如用于实现操作符重载或者在某些特殊情况下的优化。
示例:
class Date {
// 友元函数的声明
friend ostream &operator<<(ostream &out, const Date &d);
friend istream &operator>>(istream &in, Date &d);
public:
Date(int year = 0, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// <<运算符重载
ostream &operator<<(ostream &out, const Date &d) {
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
// >>运算符重载
istream &operator>>(istream &in, Date &d) {
in >> d._year >> d._month >> d._day;
return in;
}
注意: 其中cout是ostream类的一个全局对象,cin是istream类的一个全局变量,<<和>>运算符的重载函数具有返回值是为了实现连续的输入和输出操作。
说明:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类
友元类是指一个类可以将另一个类声明为它的友元,从而允许友元类访问声明它为友元的类的私有和受保护成员。这在一些特殊情况下可能很有用,但需要慎重使用,因为它可能破坏类的封装性。
示例:
class FriendClass {
public:
void display(const class MyClass& obj);
};
class MyClass {
private:
int privateData;
public:
MyClass(int data) : privateData(data) {}
// 声明 FriendClass 为友元类
friend class FriendClass;
};
// 在 FriendClass 中可以访问 MyClass 的私有成员
void FriendClass::display(const MyClass& obj) {
std::cout << "FriendClass accessing private data: " << obj.privateData << std::endl;
}
int main() {
MyClass obj(42);
FriendClass friendObj;
friendObj.display(obj);
return 0;
}
友元类说明:
1、友元关系是单向的,不具有交换性。
例如上述代码中,B是A的友元,所以在B类中可以直接访问A类的私有成员变量,但是在A类中不能访问B类中的私有成员变量。
2、友元关系不能传递。
如果A是B的友元,B是C的友元,不能推出A是C的友元。3、友元关系不具有继承性,即如果类A是类B的友元,类B的子类不会自动成为类A的友元。
内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类, 它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越 的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
#include <iostream>
using namespace std;
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A &a)
{
cout << k << endl; // OK
cout << a.h << endl; // OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A()); // 编译器会将临时对象的成员变量的值赋上默认值0
}
输出结果:
1
0
内部类的大小
class A//外部类
{
public:
class B//内部类
{
private:
int _b;
};
private:
int _a;
};
int main() {
cout << sizeof(A) << endl;//外部类的大小:4
return 0;
}
这里外部类A的大小为4,与内部类的大小无关。
匿名对象
C++ 匿名对象是指在没有被命名的情况下创建的临时对象。它们通常用于以下三种场景:
- 以值的方式给函数传参,例如
Cat();
会生成一个匿名对象,执行完后就会被析构。 - 类型转换,例如
A a = 11;
相当于A a = A(11);
,这里的A(11)
就是一个匿名对象。 - 函数需要返回一个对象时,例如
return temp;
会先生成一个匿名对象,然后返回给调用者。
匿名对象的生命周期取决于是否有其他对象接收它。如果有,那么匿名对象的生命周期就变成了接收对象的生命周期;如果没有,那么匿名对象就会在使用完后立即被销毁。
#include <iostream>
using namespace std;
class A {
public:
A(int a = 0)
: _a(a) {
cout << "A(int a)" << endl;
}
~A() {
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
int main() {
A aa1;
//A aa1(); // 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
A();
A aa2(2);
// 匿名对象在这样场景下就很好用,当然还有一些其他使用场景
int a = Solution().Sum_Solution(10);
cout << a << endl;
return 0;
}
输出结果:
A(int a)
A(int a)
~A() // 生命周期一行的临时对象A()
A(int a)
10
~A()
~A()
拷贝对象时的一些优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。
#include <iostream>
using namespace std;
class A {
public:
A(int a = 0)
: _a(a) {
cout << "A(int a)" << endl;
}
A(const A& aa)
: _a(aa._a) {
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa) {
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa) {
_a = aa._a;
}
return *this;
}
~A() {
cout << "~A()" << endl;
}
private:
int _a;
};
void func1(A aa) {
}
// 引用传递,不调用任何构造
void func2(const A& aa) {
}
int main() {
A aa1 = 1; // 构造一个临时对象 + 拷贝构造 -> 优化为直接构造(不创建临时对象)
func1(aa1); // 无优化
func1(2); // 构造一个临时对象 + 拷贝构造 -> 优化为直接构造(不创建临时对象)
func1(A(3));// 构造一个临时对象 + 拷贝构造 -> 优化为直接构造
//加引用就没有优化了,因为引用是别名,没有拷贝
func2(aa1);
//如果func2的参数不是const类型,会出现报错。aa1会创建一个临时对象,这个对象具有常属性,所以func2的参数要设置为const
func2(2); // 构造一个临时对象 + 拷贝构造 -> 优化为直接构造
func2(A(3)); // 构造一个临时对象 + 拷贝构造 -> 优化为直接构造
return 0;
}
// 创建一个A对象并返回它,会调用构造函数和拷贝构造函数
A func3() {
A aa; //构造
return aa;//拷贝构造
}
//返回一个匿名对象,会调用构造函数和拷贝构造函数。
A func4() {
return A();
}
int main() {
func3(); //构造 + 拷贝构造 -> 优化为直接构造
A aa1 = func3(); //构造 + 拷贝构造 + 拷贝构造 -> 优化为直接构造
func4(); //构造 + 拷贝构造 -> 优化为拷贝构造
A aa2 = func4(); //构造 + 拷贝构造 + 拷贝构造 -> 优化为直接构造
return 0;
}
对象返回总结:
1、接收返回值对象,尽量拷贝构造方式接收,不要赋值接收(赋值是已经定义了对象,在赋值,编译器无法优化)
2、函数中返回对象时,尽量返回匿名对象
函数传参总结: 尽量使用const &传参