面向对象编程入门:掌握C++类的基础(3/3)

在本系列的最后一篇文章中,我们将深入探讨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;
};

 【注意】

1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const 成员变量
自定义类型成员 ( 且该类没有默认构造函数时 )

 3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,

一定会先使用初始化列表初始化。

4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后
次序无关

 

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 友元函数 

问题:现在尝试去重载 operator<< ,然后发现没办法将 operator<< 重载成成员函数。 因为 cout
输出流对象和隐含的 this 指针在抢占第一个参数的位置 this 指针默认是第一个参数也就是左操作
数了。但是实际使用中 cout 需要是第一个形参对象,才能正常使用。所以要将 operator<< 重载成
全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。 operator>> 同理

 

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;
};
友元函数 可以 直接访问 类的 私有 成员,它是 定义在类外部 普通函数 ,不属于任何类,但需要在
类的内部声明,声明时需要加 friend 关键字。
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类中私有的成员变量则不行。
  • 友元关系不能传递
  • 如果CB的友元, BA的友元,则不能说明CA的友元。
  • 友元关系不能继承,在继承位置再给大家详细介绍

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,那么任何外部代码都可以访问它。如果被声明为privateprotected,则只有外部类(和其友元)可以访问。

优势和使用场景

  • 封装:内部类提供了一种强大的封装机制,允许将与外部类紧密相关但不希望暴露给外界的类隐藏起来。
  • 组织代码:对于只在某个类内部使用的辅助类,使用内部类可以保持代码的组织和清晰。
  • 实现迭代器和设计模式:内部类常用于实现迭代器,因为它可以直接访问容器的内部结构。此外,它们也适合实现某些设计模式,如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++类有了更全面的理解。面向对象编程是一种强大的编程范式,掌握它可以让你更加灵活和有效地设计软件系统。希望本系列文章能帮助你在面向对象编程的旅程上迈出坚实的一步。

  • 50
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hqxnb666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值