目录
一、初始化列表
构造函数可以使用初始化列表来对成员变量进行初始化,而不仅仅是在函数体内进行赋值操作。
初始化格式如下:
- 以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
初始化列表的特性:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量、
const成员变量、
自定义类型成员(且该类没有默认构造函数时)- 不论是否使用初始化列表,自定义类型成员变量都会先使用初始化列表进行初始化。
如果没有使用初始化列表,对未初始化的内置类型,使用随机值初始化,
对未初始化的自定类型,会自动调用默认构造- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后 次序无关。(先走初始化列表,再走函数体)
class Time
{
public:
Time(int hour = 0)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
// 特性1:每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
, _tmpi(_year)
, _tmpc('c')
, _t()//特性4:按照顺序先初始化_t再初始化_tmpc.
//特性3:如果此处舍去_t()的初始化,仍然会调用_t的构造函数
{}
private:
int _year;
int _month;
int _day;
//特性2:类中包含以下成员,必须放在初始化列表位置进行初始化,否则报错
int& _tmpi;
Time _t;
const char _tmpc;
};
注:C++11支持在类中给成员变量赋值,但是类不是实例化出来的对象,没有分配空间,所以给成员变量赋的值其实是在初始化列表使用的缺省值, 初始化列表是在类实例化对象时使用,属于某一个对象。
二、explicit 关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值 的构造函数,还具有类型转换的作用。
在C++中,explicit是一种关键字,用于修饰类的构造函数或者转换函数。
它的作用是防止编译器进行隐式类型转换。 当一个构造函数只有一个参数时,它可以被用于隐式类型转换。有时候,我们希望禁止这种隐式转换,以避免潜在的错误或者不直观的代码。这就是 explicit 关键字的用处。 以下是 explicit 的使用例子:
class MyClass {
public:
explicit MyClass(int val) // 构造函数使用 explicit 关键字
: _val(val)
{}
private:
int _val;
};
int main() {
MyClass obj1(10); // 直接调用构造函数,显式创建对象
// MyClass obj2 = 20; // 错误!禁止隐式类型转换
MyClass obj2 = MyClass(20); // 使用显式类型转换进行对象的创建
//构造函数没有被explicit修饰,下面这条代码可执行
//const MyClass& obj3 = 30;//临时对象具有常性,若是引用,需要const修饰
return 0;
}
在上面的例子中,如果MyClass的构造函数没有用explicit修饰,MyClass obj2 = 20; 是可以正常执行的,因为编译器进行隐式转换,构造了临时对象MyClass(20),然后拷贝构造给obj2。
相当于MyClass obj2 = MyClass(20);
但是MyClass的构造函数已经用explicit修饰了,所以隐式类型转换被禁止,必须使用参数相应的类型。
三、const成员
3.1 const数据成员
const 定义的常量在超出其作用域之后其空间会被释放。
在 C++ 中,const 成员变量不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。
原因:
const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其 const 数据成员的值可以不同。所以不能在类的声明中初始化 const 数据成员,因为类的对象没被创建时,编译器不知道 const 数据成员的值是什么。const 数据成员的初始化只能在类的构造函数的初始化列表中进行。
class Test {
public:
Test()
:a(0)
{}
private:
const int a;//只能在构造函数初始化列表中初始化
};
3.2 const成员函数
const修饰的“成员函数”称之为const成员函数。
const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改,但可以访问成员变量。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
}
void Print() const
{
cout << "Print()const" << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(1949, 10, 1);
d1.Print();
const Date d2(2023, 10, 1);
d2.Print();
}
对返回值加const
部分使用场景:顺序表中的[ ]重载、Print、[ ]++、....
是否对成员函数加const,取决于内部是否修改,只读函数可以加const,但是要注意const函数的匹配问题,不能出现权限的放大。
权限可以平移或缩小,但是不可以放大
- const对象不可以调用非const成员函数
- 非const对象可以调用const成员函数
- const成员函数内不可以调用其它的非const成员函数
- 非const成员函数内可以调用其它的const成员函数
四、static成员
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。 使用静态数据成员可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。
4.1 static成员变量
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为类的静态成员变量;
static成员变量的特性:
- 静态成员变量一定要在类外进行初始化。
原因:在类的内部只是声明,而静态成员变量属于类。静态成员变量不能给缺省值, 缺省值是在初始化列表使用的, 初始化列表又是在类实例化对象时使用的,属于某一个对象。通常在类的实现文件中初始化- 初始化时不加该成员的访问权限控制符private、public等
- static 关键字只能用于类定义体内部的声明中,
- static成员定义时不能标示为 static,需要标注数据类型和作用域操作符:: 指明成员属于哪个类域。前面不加static,以免与一般静态变量或对象相混淆。
- 初始化时使用作用域运算符来表明它所属的类,因此,静态数据成员是类的成员而不是对象的成员。
- 如果static成员变量被const修饰,就需要定义时在前面加上const。
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
class Test2
{
public:
Test2()
:a(0)
{}
private:
int a;
static int b;//在类的实现文件中定义并初始化
const static int c;
//static const int c;//两种写法相同
};
int Test2::b = 10;//static成员变量不能在构造函数初始化列表中初始化,因为它不属于某个对象。
const int Test2::c = 20;//注意:给静态成员常量赋值时,不需要加static修饰符,但要加cosnt。
4.2 static成员函数
用 static修饰的成员函数,称之为静态成员函数。和static成员变量一样,它们都属于类的静态成员,都不是对象成员。因此,对static成员函数的引用不需要用对象名。
static成员函数特性:
- 静态成员函数不可以调用类的非静态成员。因为静态成员函数不含this指针,非静态成员通过this指针访问。
- 静态成员是可以独立访问的,即无须创建任何对象实例就可以访问。
- 静态成员函数不可以调用非静态成员函数
- 非静态成员函数可以调用类的静态成员函数
class Test2
{
public:
Test2()
:a(0)
{}
static void func();
private:
int a;
static int b;//在类的实现文件中定义并初始化
const static int c;
};
int Test2::b = 10;//static成员变量不能在构造函数初始化列表中初始化,因为它不属于某个对象。
const int Test2::c = 20;//注意:给静态成员常量赋值时,不需要加static修饰符,但要加cosnt。
void Test2::func()
{
cout << "void Test2::func()" << endl;
}
int main()
{
Test2::func();
return 0;
}
五、友元
在C++中,友元(friend)是一种机制,通过使用友元关键字,允许在一个类中授权其他类或非成员函数访问该类的私有成员。
友元分为:友元函数和友元类
5.1 友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字。
友元函数的特性:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数。
- 友元函数不能用const修饰。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数。
- 友元函数的调用与普通函数的调用原理相同
友元函数的使用场景:
我们要是想要使用cout << D1 << endl; 将日期类对象D1输出,就需要将operator<<重载成成员函数,但是成员函数的第一个指针都是隐含的this指针,就会形成下面的例子。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
ostream& operator<<(ostream& _cout)
{
_cout << _year << "/" << _month << "/" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 10, 1);
d1.operator<<(cout);//不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
return 0;
}
虽然结果正确,但是d1.operator<<(cout);不是我们想要的写法。所以我们就需要将第一个参数给_cout,第二个给日期类,同时函数需要能够访问到Date类中的private成员。于是我们在类外定义普通函数,并且声明它为Date类的友元函数。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
friend ostream& operator<<(ostream& _cout, const Date& d1);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d1)
{
_cout << d1._year << "/" << d1._month << "/" << d1._day << endl;
return _cout;
}
int main()
{
Date d1(2023, 10, 1);
cout << d1 << endl;
return 0;
}
5.2 友元类
可以将一个类声明为另一个类的友元类,使友元类能够访问目标类的私有成员和保护成员。一个类可以将多个其他类声明为友元类,并且友元关系不具有传递性。
友元类的特性:
- 友元类的所有成员函数都可以是另一个类的友元函数。都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
- 友元关系不能传递。
- 友元关系不能继承。
class Square
{
friend class Rectangle;
public:
Square(int sideLen = 5)
:_sideLen(sideLen)
{}
private:
int _sideLen;
int _perimeter;
int _area;
};
class Rectangle
{
public:
Rectangle(int length = 0, int width = 0)
:_length(length)
,_width(width)
{}
int Area()
{
return _length * _width;
}
int Perimeter()
{
return (_length + _width) * 2;
}
void SetSquare(int sideLen)
{
//直接访问Square类私有的成员变量
_s._sideLen = sideLen;
Rectangle tmp(sideLen, sideLen);
_s._area = tmp.Area();
_s._perimeter = tmp.Perimeter();
}
private:
int _length;
int _width;
Square _s;
};
int main()
{
Rectangle r1(3,3);
r1.SetSquare(6);
return 0;
}
在上面的示例中,Rectangle 被声明为Square的友元类,Rectangle的成员函数可以直接访问Square的private成员。
友元关系应该谨慎使用,因为它可能破坏封装性,降低代码的可维护性。合理使用友元机制可以在某些特定情况下提供灵活性和方便性
六、内部类
如果一个类定义在另一个类的内部,这个内部的类就叫做内部类
内部类可以直接访问外部类的成员,包括私有成员。内部类与外部类具有一定的关联性,并且可以享有一些特殊的访问权限。但内部类是一个独立的类, 它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
内部类的特性:
- 内部类是外部类的友元类,但是外部类不是内部类的友元。
- 内部类不受外部类访问限定符的限制。
- 内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系
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;
注:内部类的定义通常放在外部类的声明中,但也可以单独定义,只需在内部类的名称前加上外部类的名称和作用域运算符(::)
class OuterClass::InnerClass{
//内部类的定义
};
内部类在某些情况下可以提供更好的封装和组织代码的方式,然而,过度使用内部类可能会增加代码的复杂性和可读性。因此,在设计类层次结构时,应考虑使用内部类的必要性和适用性。