类和对象(下)

构造函数

构造函数关于初始化列表方面

初始化列表的语法:

初始化列表的语法位于构造函数的参数列表和函数体之间,使用 : 来分隔。

class MyClass 
{
public:
    int x, y;

    // 构造函数使用初始化列表初始化 x 和 y
    MyClass(int a, int b) : x(a), y(b) {
        // 函数体可以进一步处理
    }
};

初始化列表与构造函数体内赋值的区别:

使用初始化列表:

class MyClass {
public:
    int x, y;

    // 初始化列表初始化成员变量
    MyClass(int a, int b) : x(a), y(b) {
        // 此时 x 和 y 已经被初始化
    }
};

在构造函数体内赋值:

class MyClass {
public:
    int x, y;

    // 在构造函数体内赋值
    MyClass(int a, int b) {
        x = a;
        y = b;
    }
};

虽然看起来这两种方式都能达到相同的效果,但在效率上有所不同。对于内置类型(如 intdouble),差别不大,但对于复杂类型,初始化列表可以避免不必要的临时对象创建和拷贝操作。

成员变量走初始化列表的逻辑:

显式在初始化列表中初始化的成员变量

  • 如果你在构造函数的初始化列表中明确地为成员变量指定了初始值,那么这些成员变量将按照提供的值进行初始化。
class MyClass {
public:
    int x;
    MyClass(int a) : x(a) {  // x 显式在初始化列表中被初始化为 a
    }
};

未显式在初始化列表中的成员变量

  • 如果某些成员变量没有显式在初始化列表中进行初始化,它们的初始化依赖于其声明类型:

类中声明位置有缺省值

  • 如果类内给成员变量提供了默认初始值,那么这些变量将使用该缺省值来初始化

class MyClass {
public:
    int x = 10;  // 类中声明的缺省值
    MyClass() {  // 构造函数中没有初始化列表,x 将会使用 10 作为初始值
    }
};

类中声明位置没有缺省值

  • 对于内置类型(如 intfloat 等),这些变量的初始值可能是随机的,也可能是 0 或其他默认值,这取决于编译器的行为。
  • 对于自定义类型,如果这个类型没有默认构造函数,则编译时会报错;如果有默认构造函数,编译器会调用默认构造函数进行初始化。
class MyClass {
public:
    int x;  // 没有缺省值,值可能是随机的
    MyClass() {
        // 没有初始化列表,x 的值是未定义的
    }
};

引用成员变量 / const 成员变量 / 没有默认构造函数的成员变量

  • 对于这些类型的成员变量,它们必须通过初始化列表进行初始化,否则编译器会报错。
  • 引用类型:引用一旦绑定,不能再更改,所以在对象创建时就必须初始化。
  • const 类型:常量必须在声明时初始化,因为它们的值在整个对象的生命周期内是不可改变的。
  • 没有默认构造函数的成员变量:这些类型的成员变量无法使用默认构造函数初始化,所以必须显式初始化。
class MyClass {
public:
    const int x;   // const 成员
    int& y;        // 引用成员

    MyClass(int a, int& b) : x(a), y(b) {  // 必须使用初始化列表
    }
};

总结:

  • 显式初始化列表:在初始化列表中明确初始化的成员将按指定的值进行初始化。
  • 未显式初始化:根据成员的类型(内置类型可能是随机值或 0,自定义类型调用默认构造函数)进行初始化。
  • 必须初始化列表的情况const、引用类型成员变量,以及没有默认构造函数的成员变量必须使用初始化列表初始化。

类型转换

在C++中,类型转换(Type Casting)是一种将一个类型的数据转换为另一个类型的机制。这在许多情况下很有用,例如在需要不同精度的算术运算、函数参数传递、类之间的转换等情境下。

1. 隐式类型转换

int a = 10;
double b = a;  // 隐式将 int 类型的 a 转换为 double

在上述例子中,编译器会自动将 int 类型的 a 转换为 double,以确保类型一致。

2. 显式类型转换 

C风格类型转换:

C风格的类型转换是最简单的一种方式,但它不推荐在现代 C++ 中使用,因为它不够安全和灵活,无法区分具体的转换种类。

int a = 10;
double b = (double)a;  // C 风格的显式类型转换
C++风格类型转换:

C++引入了四种更安全的类型转换运算符,用于替代C风格的转换。

1) static_cast

2) dynamic_cast

3) const_cast

4) reinterpret_cast

 隐式转换和显式转换的对比:

  • 隐式转换:自动发生,不需要程序员的干预,但可能导致数据精度丢失或行为不明确。
  • 显式转换:程序员手动进行转换,通常用于当隐式转换不能满足需求时。

 static成员

• ⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化。

• 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。

• ⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。

• 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。

• ⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。

• 突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量 和静态成员函数。

• 静态成员也是类的成员,受public、protected、private访问限定符的限制。

• 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员 变量不属于某个对象,不⾛构造函数初始化列表

1. static 成员变量 

  • 共享数据static成员变量是属于整个类的,而不是某个具体对象的。它在所有该类的对象之间共享,且只在内存中存在一份。
  • 生命周期static成员变量在程序开始时就分配内存并初始化,直到程序结束时才销毁。
  • 作用域:即使在类外也可以通过类名来访问static成员变量,而不需要实例化对象。
#include <iostream>
using namespace std;

class MyClass {
public:
    static int count; // 声明静态成员变量
    MyClass() {
        count++;
    }
};

int MyClass::count = 0; // 定义并初始化静态成员变量

int main() {
    MyClass obj1;
    MyClass obj2;
    cout << "Number of objects: " << MyClass::count << endl; // 输出2
    return 0;
}

在这个例子中,count是一个静态成员变量,它在类的所有对象之间共享。创建对象obj1obj2时,count分别递增。 

2. static 成员函数

  • 不依赖对象static成员函数可以在没有对象的情况下通过类名直接调用。它不能访问非静态成员变量或成员函数,因为它不属于类的任何实例。
  • 访问静态成员static成员函数只能访问static成员变量或其他static成员函数。
#include <iostream>
using namespace std;

class MyClass {
public:
    static int count; // 静态成员变量
    static void showCount() { // 静态成员函数
        cout << "Count: " << count << endl;
    }
};

int MyClass::count = 10;

int main() {
    MyClass::showCount(); // 通过类名直接调用静态函数
    return 0;
}

在此例子中,showCount是一个静态成员函数,它访问类的静态成员变量count,无需对象实例化即可调用。

特性总结:

  1. static成员变量在所有对象之间共享,且不依赖于任何对象实例。
  2. static成员函数可以通过类名直接调用,且不能访问非静态的成员变量或成员函数。

static成员适合存储和操作类级别的共享信息,比如计数器、全局配置等。

设已经有A,B,C,D4个类的定义,程序中A,B,C,D构造函数调⽤顺序为?()

设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调⽤顺序为?()

程序结构解析:

  1. 对象定义

    • C c; 是一个全局对象。
    • A a; 是在 main() 函数中定义的局部对象。
    • B b; 同样是 main() 中的局部对象。
    • static D d;main() 函数中的静态对象。
  2. 构造顺序: C++对象的构造顺序遵循以下规则:

    • 全局对象(如 C c)会在 main() 函数执行之前构造。
    • 局部对象(如 A aB b)按照它们在代码中出现的顺序依次构造。
    • 静态局部对象(如 static D d)在它们第一次遇到时进行构造,但它们的生命周期是整个程序的运行期间。
  3. 析构顺序

    • 局部对象的析构顺序与它们的构造顺序相反,即后构造的对象会先析构(遵循“后进先出”的原则)。
    • 静态局部对象会在程序结束时(即在 main() 函数执行完毕之后)析构。
    • 全局对象会在 main() 函数执行结束后,所有静态局部对象析构完毕后析构。

构造函数调用顺序:

  1. 全局对象 C:首先构造,因为它是全局变量,在 main() 函数执行之前构造。
  2. 局部对象 A:在 main() 中第一个被声明,因此紧随其后构造。
  3. 局部对象 B:在 main() 中第二个被声明,接着构造。
  4. 静态局部对象 D:在 main() 函数中遇到 static D d; 时构造。

析构函数调用顺序:

  1. 局部对象 BB 是局部对象,main() 函数结束时,B 最后构造,最先析构。
  2. 局部对象 AA 是局部对象,在 B 之后析构。
  3. 静态局部对象 DD 是静态局部对象,main() 函数结束后析构。
  4. 全局对象 CC 是全局对象,最后析构。
  • 构造函数调用顺序:C -> A -> B -> D
  • 析构函数调用顺序:B -> A -> D -> C

 友元:

• 友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。

• 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。

• 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。

• ⼀个函数可以是多个类的友元函数。

• 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。

• 友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。

• 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。

• 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。

友元的类型:

  1. 友元函数(Friend Function)
  2. 友元类(Friend Class)
  3. 友元成员函数(Member Function as Friend)

1. 友元函数(Friend Function)

 友元函数是一个不是类成员的函数,但它可以访问该类的所有私有成员和保护成员。要定义友元函数,可以在类定义中使用 friend 关键字声明。

#include <iostream>
using namespace std;

class MyClass {
private:
    int x;
public:
    MyClass(int val) : x(val) {}

    // 声明友元函数
    friend void showValue(MyClass& obj);
};

// 定义友元函数
void showValue(MyClass& obj) {
    cout << "x = " << obj.x << endl;  // 访问私有成员 x
}

int main() {
    MyClass obj(10);
    showValue(obj);  // 调用友元函数
    return 0;
}
  • showValue()MyClass 类的友元函数,它能访问 MyClass 的私有成员 x
  • 尽管 x 是私有成员,但友元函数可以访问它。

2. 友元类(Friend Class)

友元类允许一个类访问另一个类的所有私有和保护成员。在类定义中,可以使用 friend 关键字声明另一个类为其友元类。

#include <iostream>
using namespace std;

class MyClass {
private:
    int x;
public:
    MyClass(int val) : x(val) {}

    // 声明 FriendClass 是 MyClass 的友元类
    friend class FriendClass;
};

class FriendClass {
public:
    void showValue(MyClass& obj) {
        cout << "x = " << obj.x << endl;  // 访问私有成员 x
    }
};

int main() {
    MyClass obj(20);
    FriendClass fObj;
    fObj.showValue(obj);  // 通过友元类访问私有成员
    return 0;
}
  • FriendClassMyClass 的友元类,因此它能够访问 MyClass 中的私有成员 x
  • FriendClass::showValue() 访问了 MyClass 的私有成员 x。

3. 友元成员函数(Member Function as Friend)

你还可以将另一个类的某个成员函数声明为当前类的友元。这允许特定的成员函数访问当前类的私有成员,而不是整个类。

#include <iostream>
using namespace std;

class ClassB;  // 前向声明

class ClassA {
private:
    int x;
public:
    ClassA(int val) : x(val) {}

    // 声明 ClassB 的某个成员函数是 ClassA 的友元
    friend void ClassB::show(ClassA& obj);
};

class ClassB {
public:
    void show(ClassA& obj) {
        cout << "x = " << obj.x << endl;  // 访问 ClassA 的私有成员 x
    }
};

int main() {
    ClassA objA(30);
    ClassB objB;
    objB.show(objA);  // 通过 ClassB 的成员函数访问 ClassA 的私有成员
    return 0;
}
  • ClassBshow() 函数是 ClassA 的友元成员函数,因此它可以访问 ClassA 的私有成员 x
  • ClassB 本身不是 ClassA 的友元类,只有 show() 函数有权访问 ClassA 的私有成员。

注意事项:

  1. 友元破坏封装性:友元虽然方便,但它也破坏了面向对象编程的封装性原则,因此应谨慎使用。滥用友元会导致代码的维护变得困难,因为它打破了类之间的清晰边界。

  2. 友元不是继承的一部分:友元关系是非对称和非传递的。如果 ClassAClassB 的友元,ClassB 不会自动成为 ClassA 的友元。类似地,友元关系不会在继承中传递。

  3. 使用友元需慎重:尽量保持类的成员变量私有,只有在确实需要其他函数或类直接访问内部数据时才使用友元。这是一种增强代码灵活性的方法,但应在设计时权衡其副作用。

总结:

  • 友元函数:允许非成员函数访问类的私有成员。
  • 友元类:允许另一个类访问当前类的所有成员。
  • 友元成员函数:允许特定的成员函数访问类的私有成员。
  • 友元提供了灵活的访问控制方式,但应慎用,以维护良好的封装性和代码结构。

 内部类:

• 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在 全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。

• 内部类默认是外部类的友元类。

• 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其 他地⽅都⽤不了

求1+2+3+...+n_牛客题霸_牛客网 这题可以让我们更加了解什么是内部类

class Solution {
    // 内部类
    class Sum
    {
    public:
        Sum()
        {
            _ret += _i;
            ++_i;
        }
    };
    static int _i;
 static int _ret;
 public:
    int Sum_Solution(int n) 
    {
        // 变⻓数组
        Sum arr[n];
        return _ret;
    }
 };
 int Solution::_i = 1;
 int Solution::_ret = 0;

匿名对象

匿名对象(Anonymous Object)是指没有名字的对象,即对象在创建时不分配一个显式的变量名。这类对象通常是临时对象,创建后会立即用于某个操作,且在操作完成后就会销毁。匿名对象一般在函数调用、返回值或赋值时出现,并且生命周期非常短。

匿名对象的使用场景:

  1. 函数的临时返回值: 当函数返回一个对象时,往往返回的是一个匿名对象。这个匿名对象可以被赋值给另一个变量,也可以作为临时对象直接使用。

  2. 临时对象的创建: 可以直接通过构造函数创建一个匿名对象,匿名对象只会在表达式的上下文中存活,使用完之后会立即销毁。

匿名对象的特征:

  1. 没有名称:匿名对象没有名字,因此你无法直接引用它,通常只能在表达式中使用。
  2. 生命周期短:匿名对象的生命周期非常短,只在当前的表达式范围内存活,表达式结束后,匿名对象就会被销毁。
  3. 临时性:它们是临时对象,通常用于一些不需要长期保存的场景。

匿名对象的例子:

1. 函数返回匿名对象:

#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass() {
        cout << "构造函数被调用" << endl;
    }
    
    ~MyClass() {
        cout << "析构函数被调用" << endl;
    }
};

MyClass createObject() {
    return MyClass();  // 返回一个匿名对象
}

int main() {
    MyClass obj = createObject();  // 调用构造和析构函数
    return 0;
}

2. 直接创建匿名对象:

#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass() {
        cout << "构造函数被调用" << endl;
    }

    ~MyClass() {
        cout << "析构函数被调用" << endl;
    }
};

int main() {
    MyClass();  // 创建了一个匿名对象
    cout << "匿名对象创建完毕" << endl;
    return 0;
}

3. 作为函数参数的匿名对象:

#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass() {
        cout << "构造函数被调用" << endl;
    }

    ~MyClass() {
        cout << "析构函数被调用" << endl;
    }
};

void func(MyClass obj) {
    cout << "函数体内" << endl;
}

int main() {
    func(MyClass());  // 传递一个匿名对象作为参数
    return 0;
}

匿名对象的作用:

  1. 减少内存开销: 匿名对象通常用于那些只需要临时存在的对象场景。它们在创建之后立即使用,使用完后销毁,这样就不会占用多余的内存。

  2. 简化代码: 匿名对象可以让代码更加简洁,因为不需要为临时对象定义名称,直接使用对象的构造函数创建并使用。

  3. 优化性能: 现代C++编译器支持的返回值优化(RVO)移动语义可以减少匿名对象的开销。尤其是通过移动构造函数,将匿名对象的资源“移动”到目标对象,而不是进行拷贝。

匿名对象的生命周期:

  1. 临时对象的创建:当表达式或函数需要时,匿名对象会立即创建。
  2. 临时对象的销毁:一旦表达式结束,匿名对象会被立即销毁。析构函数会自动被调用。

匿名对象的注意事项:

  1. 生命周期短:由于匿名对象的生命周期非常短,可能在你希望使用它之前就已经销毁了。因此,匿名对象不适合存储或传递给需要长期使用的对象。

  2. RVO(返回值优化):C++ 编译器可以通过优化,避免拷贝和构造不必要的临时对象。例如,在函数返回匿名对象时,编译器可能直接将返回值“构造”在目标位置,而不创建中间对象。

  3. 无法直接引用:匿名对象没有名称,无法在程序的其他地方直接引用它,只能通过当前的上下文使用它。

总结:

  • 匿名对象 是一种不具名的临时对象,通常在函数返回值、参数传递和临时计算时使用。
  • 生命周期非常短,它们在表达式结束后立即销毁,析构函数会自动调用。
  • 减少了冗余对象的创建,有助于简化代码并优化性能。

对象拷⻉时的编译器优化

在C++中,当对象拷贝时,编译器可以进行一些优化来减少不必要的对象构造和销毁操作,尤其是对于临时对象和返回值的拷贝。主要的编译器优化机制包括:

  1. 返回值优化(RVO)命名返回值优化(NRVO)
  2. 移动语义和移动构造函数
  3. 拷贝省略(Copy Elision)

1. 返回值优化(RVO)和命名返回值优化(NRVO)

返回值优化(RVO) 是编译器为避免不必要的临时对象拷贝而进行的一种优化技术。当一个函数返回对象时,编译器可以直接在调用代码的目标位置构造返回的对象,而不是创建临时对象再拷贝。这个优化在C++98中就已经存在,并且在C++17中被强制要求成为标准行为,不再需要编译器特定的优化。

命名返回值优化(NRVO) 是RVO的变体,用于处理函数中定义的具名变量的返回优化。编译器可以直接在返回值位置构造这个具名变量,从而避免额外的拷贝和构造操作。

2. 移动语义和移动构造函数

C++11引入了移动语义,包括移动构造函数移动赋值运算符。移动语义允许编译器将资源从一个临时对象“移动”到目标对象,而不是进行昂贵的拷贝操作。这对于避免不必要的深拷贝非常有用,尤其是在处理动态内存、文件句柄等资源时。

3. 拷贝省略(Copy Elision)

拷贝省略 是C++中最常见的优化之一。即使没有移动语义,编译器也可以直接跳过拷贝操作(拷贝构造函数的调用),并直接将对象构造在目标内存位置。这与RVO和NRVO类似,但它不仅适用于返回值场景,也适用于对象赋值和传递的其他场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值