在本系列的最后一篇文章中,我们将深入探讨C++类的高级特性,帮助你更全面地理解面向对象编程。我们将从构造函数的进一步讨论开始,然后依次探讨静态成员、友元、内部类、匿名对象、拷贝对象时的编译器优化,最后再次理解封装的概念。
1. 再谈构造函数
1.1 初始化列表
初始化列表是构造函数中用于初始化成员变量的一种语法。它直接在构造函数的冒号后列出,允许成员变量在构造函数体执行之前就被初始化。这不仅使代码更加清晰,还能提高效率,特别是对于那些没有默认构造函数的类类型成员或者是基类。
初始化列表:以一个 冒号开始 ,接着是一个以 逗号分隔的数据成员列表 ,每个 " 成员变量 " 后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
【注意】
引用成员变量const 成员变量自定义类型成员 ( 且该类没有默认构造函数时 )
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,
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();
}
A. 输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
选择D
1.2 explicit关键字
在C++中,构造函数如果只接受一个参数(或所有参数除了一个都有默认值),它可以被用作隐式类型转换。虽然这在某些情况下很方便,但也可能导致意外的类型转换,从而引入错误。为了防止这种隐式转换,可以在单参数构造函数前加上explicit
关键字。
class MyClass {
public:
explicit MyClass(int x) {
// 构造函数的实现
}
};
void function(MyClass obj) {
// 函数实现
}
int main() {
MyClass obj = MyClass(10); // 正确
// function(10); // 错误:不能隐式转换int到MyClass
function(MyClass(10)); // 正确:显式转换
}
2. Static成员
在类中,静态成员是共享的,不属于任何单个对象。静态成员可以是变量也可以是函数。
- 静态成员变量对于类的所有对象来说是共有的,可以用来存储类级别的数据。
class MyClass { public: static int staticValue; }; int MyClass::staticValue = 0; // 静态成员变量的定义和初始化 int main() { MyClass obj1, obj2; obj1.staticValue = 5; std::cout << obj2.staticValue << std::endl; // 输出5,显示obj1和obj2共享同一个staticValue }
- 静态成员函数则可以在没有任何对象实例的情况下被调用,但它只能访问静态成员变量和其他静态成员函数。
class MyClass {
public:
static int staticValue;
static void displayStaticValue() {
std::cout << staticValue << std::endl;
}
};
int MyClass::staticValue = 42;
int main() {
MyClass::displayStaticValue(); // 直接通过类名调用静态成员函数
}
使用场景和注意事项
- 全局数据共享:静态成员变量可以用于在类的所有实例之间共享数据。
- 独立函数:静态成员函数可以作为独立于任何对象的函数使用,特别是当函数的行为不依赖于对象状态时。
- 访问控制:静态成员既可以是公有的也可以是私有的。私有静态成员可以用于实现类内部的帮助函数或数据,而公有静态成员可以提供对全局类数据的访问。
- 初始化:静态成员变量需要在类外部进行定义和初始化,即使它们已在类内部声明。
-
1. 静态成员 为 所有类对象所共享 ,不属于某个具体的对象,存放在静态区2. 静态成员变量 必须在 类外定义 ,定义时不添加 static 关键字,类中只是声明3. 类静态成员即可用 类名 :: 静态成员 或者 对象 . 静态成员 来访问4. 静态成员函数 没有 隐藏的 this 指针 ,不能访问任何非静态成员5. 静态成员也是类的成员,受 public 、 protected 、 private 访问限定符的限制
3. 友元
友元函数或友元类提供了一种访问类的私有或受保护成员的方法。通过将某个函数或整个类声明为另一个类的友元,可以使其访问后者的非公开成员。
友元关系不是相互的,也不是可继承的。
友元分为: 友元函数 和 友元类
3.1 友元函数
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
3.2 友元类
- 友元关系是单向的,不具有交换性。
- 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接
- 访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递
- 如果C是B的友元, B是A的友元,则不能说明C时A的友元。
- 友元关系不能继承,在继承位置再给大家详细介绍
4. 内部类
内部类,也称为嵌套类,是定义在另一个类内部的类。这种结构允许内部类访问外部类的私有成员,同时对外界隐藏其实现细节。内部类在C++中是一种强大的封装工具,它可以用于多种场景,如实现迭代器、设计模式或简单地组织代码。让我们深入了解内部类的特性、用法及其优势。
特性和用法
内部类定义在另一个类的作用域内,但它完全是一个独立的类。因此,内部类可以有自己的成员变量、成员函数、构造函数和析构函数等。
class OuterClass {
public:
OuterClass() : inner() {} // 外部类构造函数
void display() {
inner.display();
}
class InnerClass { // 内部类定义
public:
void display() {
std::cout << "InnerClass display function" << std::endl;
}
};
private:
InnerClass inner; // 外部类的成员变量
};
在上面的例子中,InnerClass
是一个内部类,它定义在OuterClass
内部。OuterClass
可以自由访问InnerClass
的公有成员,包括构造函数和display
函数。
访问控制
内部类遵循正常的访问控制规则。如果内部类被声明为public
,那么任何外部代码都可以访问它。如果被声明为private
或protected
,则只有外部类(和其友元)可以访问。
优势和使用场景
- 封装:内部类提供了一种强大的封装机制,允许将与外部类紧密相关但不希望暴露给外界的类隐藏起来。
- 组织代码:对于只在某个类内部使用的辅助类,使用内部类可以保持代码的组织和清晰。
- 实现迭代器和设计模式:内部类常用于实现迭代器,因为它可以直接访问容器的内部结构。此外,它们也适合实现某些设计模式,如Builder或Factory模式,这些模式中的私有实现细节可以通过内部类隐藏起来。
注意事项
- 依赖关系:内部类与外部类之间存在明显的依赖关系。如果外部类的接口或实现发生变化,可能需要相应地更新内部类。
- 内存占用:虽然内部类是独立的类,但是每个内部类的对象都会增加外部类对象的内存占用。因此,应当谨慎使用,避免不必要的资源消耗。
5. 匿名对象
匿名对象是没有名称的对象,在创建时不需要显式地指定变量名。它们通常用于立即调用对象的方法或者作为函数参数,而不打算在之后的代码中再次引用该对象。匿名对象是临时的,它们的生命周期仅限于创建它们的表达式执行期间。在C++中,匿名对象的使用可以简化代码,同时也有助于优化性能。
使用场景
匿名对象主要用于以下几种场景:
- 一次性使用的对象:当对象只需要被使用一次,之后不再需要时,使用匿名对象可以避免为临时对象命名。
- 函数参数:如果函数需要一个对象作为参数,而这个对象在之后的代码中不再被使用,可以直接在函数调用时创建一个匿名对象作为参数。
- 对象初始化:在某些情况下,匿名对象可以用于初始化另一个对象或数组中的元素。
示例
考虑一个简单的类Book
,它有一个显示书籍信息的成员函数:
class Book {
public:
Book(std::string title) : title_(title) {}
void display() const {
std::cout << "Book: " << title_ << std::endl;
}
private:
std::string title_;
};
一次性使用
如果我们只是想打印一本书的信息,并且之后不再需要这个Book
对象,可以使用匿名对象:
Book("C++ Primer").display();
在这个例子中,一个匿名的Book
对象被创建并立即用于调用display
方法。对象的生命周期结束于display
方法调用的结束。
作为函数参数
假设有一个函数需要Book
对象作为参数:
void printBookInfo(const Book& book) {
book.display();
}
调用这个函数时,可以直接传递一个匿名对象
printBookInfo(Book("Effective C++"));
优点:
- 简化代码:匿名对象避免了为一次性使用的对象命名,使代码更加简洁。
- 性能优化:在某些情况下,使用匿名对象可以减少对象的创建和销毁开销,尤其是当编译器进行返回值优化等优化时。
注意事项:
- 生命周期管理:由于匿名对象的生命周期很短,只在创建它的表达式中有效,因此需要注意不要在其生命周期结束后尝试访问它。
- 资源管理:如果匿名对象涉及到资源的分配(如动态内存分配),需要确保资源能够在对象生命周期结束时正确释放,以避免资源泄露。
6. 拷贝对象时的一些编译器优化
在C++中,对象的拷贝操作是一个重要但有时代价昂贵的过程。为了提高效率,C++编译器会在某些情况下应用优化技术,减少不必要的拷贝。这些优化包括(但不限于)拷贝省略(Copy Elision)和返回值优化(Return Value Optimization, RVO)。理解这些优化对于编写高效的C++代码非常重要。
拷贝省略(Copy Elision)
拷贝省略是编译器用来避免不必要拷贝的一种优化技术。C++标准允许在某些情况下省略临时对象的创建和拷贝,即使这种省略会改变程序的行为。拷贝省略最常见的场景包括:
- 临时对象的直接初始化:当一个对象被另一个同类型的临时对象初始化时,编译器可以省略临时对象的创建和拷贝。
- 函数返回对象时:当函数返回一个局部对象时,编译器可以省略拷贝或移动构造函数的调用,直接在调用方的上下文中构造返回值。
返回值优化(RVO)和命名返回值优化(NRVO)
返回值优化是拷贝省略的一个特例,它涉及到函数返回对象时的优化。RVO允许编译器在源对象的存储位置直接构造函数的返回值,避免了拷贝或移动构造函数的调用。如果返回的是一个局部变量(命名返回值优化,NRVO),编译器同样可以优化掉拷贝或移动。
class MyClass {
public:
MyClass() {}
MyClass(const MyClass&) {
std::cout << "Copy constructor called" << std::endl;
}
};
MyClass createObject() {
MyClass obj;
return obj; // NRVO可能会在这里发生,避免拷贝构造函数的调用
}
int main() {
MyClass myObj = createObject(); // RVO可能会在这里发生
}
createObject
函数创建了一个MyClass
类型的局部对象obj
。按照C++的一般规则,当obj
作为返回值时,它应该被拷贝构造到函数外部的一个临时位置,然后再从这个临时位置拷贝构造到main
函数中的myObj
。这将涉及到两次拷贝构造调用。
然而,由于NRVO,编译器允许直接在myObj
的内存位置上构造obj
,从而避免了拷贝构造函数的调用。因此,尽管代码中定义了拷贝构造函数,但由于NRVO的作用,它没有被调用。
通过深入探讨这些高级特性,你现在应该对C++类有了更全面的理解。面向对象编程是一种强大的编程范式,掌握它可以让你更加灵活和有效地设计软件系统。希望本系列文章能帮助你在面向对象编程的旅程上迈出坚实的一步。