<C++> 类和对象(下)

1.const成员

const 成员是指在类中声明的成员(变量或函数),其值或行为在对象的整个生命周期中都是不可修改的。这有助于确保对象的某些属性或行为不会被意外地更改,从而提高代码的可靠性和可维护性。

C++中有两种使用 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成员的权限问题

问题:

  1. const对象可以调用非const成员函数吗? 答:不可以。因为const对象的成员函数是const的,而非const成员函数是不const的,两者不匹配。如果强制调用,会导致编译错误。
  2. 非const对象可以调用const成员函数吗? 答:可以。因为非const对象的成员函数既可以是const的,也可以是非const的,两者匹配。
  3. const成员函数内可以调用其他的非const成员函数吗? 答:不可以。因为const成员函数不能修改对象的状态,而非const成员函数可能会修改对象的状态,这样会导致编译错误。
  4. 非cosnt成员函数内可以调用其他的cosnt成员函数吗? 答:可以。因为非cosnt成员函数既可以是cosnt的,也可以是非cosnt的,两者匹配。

2.static成员

静态成员是类的成员,它们与类本身相关,而不是与类的实例(对象)相关。静态成员在所有类的实例之间共享,并且可以通过类名访问,而不需要创建类的实例。C++中有两种类型的静态成员:静态数据成员静态成员函数

静态数据成员

这是类的成员变量,它在类的所有实例之间共享。静态数据成员在类的声明中被声明为static,但是它们需要在类外部进行定义和初始化。通常,这是为了确保类的所有实例共享同一个数据。

class MyClass {
    
    
private:
    static int staticVar;  // 静态数据成员的声明
};

int MyClass::staticVar = 0;  // 静态数据成员的定义和初始化

int main() {
    
    
    MyClass obj1;
    MyClass obj2;

    obj1.staticVar = 5;
    std::cout << obj2.staticVar;  // 输出为 5,因为静态数据成员在所有实例之间共享
    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时,尽管我们突破了类域,也不能对其进行访问。

总结:

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

注意区分两个问题:
1、静态成员函数可以调用非静态成员函数吗?
2、非静态成员函数可以调用静态成员函数吗?
问题1:不可以。因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数。
问题2:可以。因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符的限制。

3.友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以 友元不宜多用。

友元分为:友元函数友元类

友元函数

友元函数是一种特殊类型的函数,它被允许访问类的私有成员,尽管这些函数不是类的成员。友元函数通常在类的声明中通过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的友元。

4.内部类

内部类是一个类定义在另一个类的内部的类。也就是说,一个类可以在另一个类的内部声明和定义一个类,被声明的类被称为内部类。内部类可以访问外部类的成员,包括私有成员,因为它们被视为外部类的友元

注意
 1、此时的内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象区调用内部类。
 2、外部类对内部类没有任何优越的访问权限。
 3、内部类就是外部类的友元类,即内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

特性
 1、内部类可以定义在外部类的public、private以及protected这三个区域中的任一区域。
 2、内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
 3、外部类的大小与内部类的大小无关。

class A//外部类
{
    
    
public:
    class B//内部类
    {
    
    
    private:
        int _b;
    };

private:
    int _a;
};

int main() {
    
    
    cout << sizeof(A) << endl;//外部类的大小:4
    return 0;
}

这里外部类A的大小为4,与内部类的大小无关。

5.匿名对象

C++ 匿名对象是指在没有被命名的情况下创建的临时对象。它们通常用于以下三种场景:

  • 以值的方式给函数传参,例如 Cat(); 会生成一个匿名对象,执行完后就会被析构。
  • 类型转换,例如 A a = 11; 相当于 A a = A(11);,这里的 A(11) 就是一个匿名对象。
  • 函数需要返回一个对象时,例如 return temp; 会先生成一个匿名对象,然后返回给调用者。

匿名对象的生命周期取决于是否有其他对象接收它。如果有,那么匿名对象的生命周期就变成了接收对象的生命周期;如果没有,那么匿名对象就会在使用完后立即被销毁。

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);
    // 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说
    Solution().Sum_Solution(10);
    return 0;
}

6.拷贝对象时的一些优化

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还 是非常有用的。

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));// 构造+拷贝构造 -> 优化为直接构造   A(3)构造  传参拷贝构造

    // 加引用就没有优化了,因为引用是别名,没有拷贝
    func2(aa1);
    func2(2);// 如果func2的参数不是const类型,会出现报错。aa1会创建一个临时对象,这个对象具有常属性,所以func2的参数要设置为const
    func2(A(3));
    return 0;
}
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 &传参

猜你喜欢

转载自blog.csdn.net/ikun66666/article/details/132503843