学习笔记—C++—类和对象(三)

发布于:2025-04-14 ⋅ 阅读:(22) ⋅ 点赞:(0)

目录

类和对象

再探构造函数

类型转换

隐式类型转换

显式类型转换

C语言风格类型转换

C++风格类型转换

static_cast

dynamic_cast

const_cast

reinterpret_cast

static成员

友元

友元函数

友元类

友元成员函数

内部类

匿名对象

匿名对象的使用场景:

匿名对象的作用:

匿名对象的特点:

匿名对象的生命周期:

对象拷贝时的编译器优化


类和对象

再探构造函数

 之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有⼀种方

式,就是初始化列表,初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成

员列表,每个"成员变量"后面跟⼀个放在括号中的初始值或表达式

每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方

引用成员变量const成员变量没有默认构造的类类型变量必须放在初始化列表位置进行初始化,否则会编译报错

C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的

尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的⾃定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。

初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序初始化列表顺序保持⼀致

class Time
{
public :
	Time(int hour)
		: _hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};
class Date
{
public :
	Date(int& x, int year = 1, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
		, _t(12)
		, _ref(x)
		, _n(1)
	{
		// error C2512: “Time”: 没有合适的默认构造函数可⽤
		// error C2530 : “Date::_ref” : 必须初始化引⽤
		// error C2789 : “Date::_n” : 必须初始化常量限定类型的对象
	} 
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
    // 声明给缺省值 ->初始化列表
	int _year = 3; //在没有缺省值的情况下这个3就成缺省值的备胎一样代替缺省值
	int _month;
	int _day;
	Time _t;      // 没有默认构造
	int& _ref;    // 引⽤
	const int _n; // const
};
int main()
{
	int i = 0;
	Date d1(i);
	d1.Print();
	return 0;
}

三类必须在初始化列表初始化:引用成员变量,const成员变量,没有默认构造的类类型成员变量。

从下往上,先初始化_a2,但_a1还没有初始化呢,所以_a2就是随机值;_a1则使用的是被赋值的1。

类型转换

● C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。

● 构造函数前面加explicit就不再支持隐式类型转换。

● 类类型的对象之间也可以隐式转换,需要相应的构造函数支持。

隐式类型转换

隐式类型转换是由编译器自动完成的,通常发生在不同类型的变量之间进行操作时。C++ 会在合理的情况下自动转换类型,如将 int 转换为 double

int a = 10;
double b = 5.5;
double c = a + b;  // a 被自动转换为 double 类型

隐式转换虽然方便,但也可能会带来精度丢失或效率问题。例如 double 转换为 int 会丢失小数部分。

显式类型转换

C语言风格类型转换

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

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

显式类型转换通常通过强制转换(Type Casting)实现,程序员通过明确的语法告诉编译器进行类型转换。

C++ 提供了四种强制类型转换方法:

● static_cast

 dynamic_cast

 const_cast

● reinterpret_cast

static_cast

示例:

int a = 10;
double b = static_cast<double>(a);  // 将 int 类型转换为 double 类型

这是最常用的类型转换,适用于大多数基本类型之间的转换。它在编译期进行检查,不涉及运行时开销。

dynamic_cast

示例:

class Base {
    virtual void func() {}
};

class Derived : public Base {
    void func() override {}
};

Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base);  // 基类转换为派生类

dynamic_cast 主要用于多态类型(带有虚函数的类)之间的转换,用于将基类指针或引用转换为派生类指针或引用,运行时检查转换是否成功,若失败,指针会返回 nullptr

const_cast

示例:

const int a = 10;
int* p = const_cast<int*>(&a);  // 去掉 const 属性
*p = 20;  // 这样做有风险,修改真正的常量可能导致未定义行为

const_cast 用于去掉或添加 const 属性。它主要用来修改常量变量,但请注意,不要试图通过它修改真正的常量

reinterpret_cast

示例:

int a = 42;
char* p = reinterpret_cast<char*>(&a);  // 将 int* 转换为 char*

这种转换最不安全,它将一种类型的指针或引用转换为另一种类型的指针或引用,主要用于底层操作,例如将 int 类型的指针转换为 char*

隐式和显式对比:

● 隐式转换:自动发生,不需要程序员的干预,但可能导致数据精度丢失或行为不明确。

● 显式转换:程序员手动进行转换,通常用于当隐式转换不能满足需求时。

static成员

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

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

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

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

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

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

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

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

求1+2+3+...+n_⽜客题霸_⽜客⽹

class Sum
{
public:
    Sum()//构造函数,每当创建了Sum类的对象就会自动进行调用例如
    {
        _ret+=_i;
        ++_i;
    }
    static int GetRet()
    {
        return _ret;
    }
private:
    static int _i;
    static int _ret;
};
int Sum::_i=1;
int Sum::_ret=0;

class Solution {
public:
    int Sum_Solution(int n)
    {
        Sum arr[n];//创建一个有n个元素的数组
        return Sum::GetRet();
        
        //当这个数组被创建时,数组的每一个元素都会触发 Sum 类的构造函数,从而实现累加运算。
    }
    
};

因为c是全局的,所以我们在main函数之前就可以进行初始化操作了。

对于析构函数的话,后定义的先进行析构操作,这里我们先析构ab,因为cd的声明周期是全局的,一定在main韩函数结束以后才会进行析构的操作。

局部的静态是会先进行析构的,全局后析构。

1. 静态成员变量的声明
在类内部声明静态成员变量时,使用 static 关键字。

示例:

class MyClass {
public:
    static int staticVar;  // 静态成员变量声明
};


在类 MyClass 中,我们声明了一个静态的整型变量 staticVar。注意,这里仅仅是声明。

2. 静态成员变量的初始化
静态成员变量需要在类外进行初始化,初始化时不使用 static 关键字,只需要指定其类型和类作用域。

示例:

class MyClass {
public:
    static int staticVar;  // 静态成员变量声明
};

// 在类外部进行初始化
int MyClass::staticVar = 10;

int main() {
    // 访问静态成员变量
    std::cout << "Static Variable: " << MyClass::staticVar << std::endl;

    // 修改静态成员变量
    MyClass::staticVar = 20;
    std::cout << "Updated Static Variable: " << MyClass::staticVar << std::endl;

    return 0;
}

3. 解释:
● class MyClass 中,我们声明了静态成员变量 staticVar

● 然后,在类的外部(在 main 函数之外的全局范围内),我们通过 MyClass::staticVar 进行静态成员变量的初始化。在这个过程中,我们指定它的初始值为 10。

之后,静态成员变量可以通过类名 MyClass::staticVar 进行访问和修改。

4. 静态成员变量的特点
● 静态成员变量只在类的作用域内声明一次,但会在类的所有对象中共享。

● 即使没有创建对象,也可以通过 类名::静态成员变量 进行访问。

● 它只需要在类外初始化一次,无论创建多少个对象,静态成员变量都不会被重新初始化。

● 静态成员变量的生命周期与程序的生命周期相同,直到程序结束时才会被销毁。

5. 静态成员变量的访问方式:
● 可以通过类名来访问静态成员变量:MyClass::staticVar

● 也可以通过类的对象来访问静态成员变量,但这并不是推荐的方式。

示例:

MyClass obj;
std::cout << "Static Variable via object: " << obj.staticVar << std::endl;  // 不推荐

6. 静态常量成员变量
对于常量静态成员变量(如 const static),可以在类内部进行初始化,但前提是它的类型必须是整型或枚举类型。

示例:

class MyClass {
public:
    static const int constVar = 100;  // 可以在类内部直接初始化
};

如果静态常量成员是非整数类型,则必须像普通静态成员一样,在类外进行初始化。

示例:

class MyClass {
public:
    static const double constDouble;
};

// 类外初始化
const double MyClass::constDouble = 3.14;

静态成员变量在类内声明,在类外进行初始化。

静态成员变量属于类本身,而不是某个具体的对象。

静态成员变量可以通过类名访问,也可以通过对象访问(但不推荐)。

静态常量成员变量如果是整型或枚举类型,可以在类内直接初始化;否则,必须在类外进行初始化。

友元

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

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

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

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

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

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

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

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

友元函数

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

class MyClass 
{
    // 声明友元函数
    friend void showValue(MyClass& obj);
private:
    int x;
public:
    MyClass(int val) : x(val) {}
 
};
 
// 定义友元函数
void showValue(MyClass& obj) {
    cout << "x = " << obj.x << endl;  // 访问私有成员 x
}
 
int main() {
    MyClass obj(10);
    showValue(obj);  // 调用友元函数
    return 0;
}

showValue() 是 MyClass 类的友元函数,它能访问 MyClass 的私有成员 x

友元类

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

class MyClass 
{
    // 声明 FriendClass 是 MyClass 的友元类
    friend class FriendClass;
private:
    int x;
public:
    MyClass(int val) : x(val) {}
 
};
 
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;
}

FriendClass 是 MyClass 的友元类,因此FriendClass::showValue() 访问了 MyClass 中的私有成员 x

友元成员函数

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

​class ClassB;  // 前向声明
 
class ClassA 
{
    // 声明 ClassB 的某个成员函数是 ClassA 的友元
    friend void ClassB::show(ClassA& obj);
private:
    int x;
public:
    ClassA(int val) : x(val) {}
 
};
 
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;
}

​

​

ClassB 的 show() 函数是 ClassA 的友元成员函数,因此它可以访问 ClassA 的私有成员 x

ClassB 本身不是 ClassA 的友元类,只有 show() 函数有权访问 ClassA 的私有成员。

友元函数:允许非成员函数访问类的私有成员。

友元类:允许另一个类访问当前类的所有成员。

友元成员函数:允许特定的成员函数访问类的私有成员。

内部类

●  如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
●  内部类默认是外部类的友元类。
●  内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其
他地方都用不了。
class A
{
private:
	static int _k;
	int _h = 1;

public:
	class B // B默认就是A的友元
	{
	public:
		void foo(const A& a)
		{
			cout << _k << endl; //OK
			cout << a._h << endl; //OK
		}
	private:
		int _b;
	};
};

int A::_k = 1;

int main()
{
	cout << sizeof(A) << endl;

	A::B b;

	return 0;
}

内部类是外部的友元,只能通过外部才能调内部,内部类存储在静态区。

匿名对象

 用类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象

匿名对象生命周期只在当前一行,⼀般临时定义⼀个对象当前用⼀下即可,就可以定义匿名对象。

匿名对象的使用场景:

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

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

匿名对象的作用:

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

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

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

匿名对象的特点:

  1. 没有名称:匿名对象没有明确的变量名,它们直接在创建的地方使用。

  2. 短生命周期:通常,它们的生命周期很短,通常仅限于创建它们的上下文。

  3. 简化代码:匿名对象有助于避免为临时对象命名,减少不必要的代码。

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;
	}
};
void func(A aa=A(1))
{}


int main()
{

	func();

	//有名对象
	A aa1(1);
	A aa2;

	//匿名对象
	A(1);
	A();

	//这种事有名对象调用函数的操作
	Solution s1;
	cout << s1.Sum_Solution(10) << endl;

	//下面是匿名对象调用函数
	cout << Solution().Sum_Solution(10) << endl;

	const A& r = A();
	return 0;
}



匿名对象的生命周期:

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

注意事项:匿名对象虽然能简化代码,但也可能让代码变得难以调试或维护,因为没有明确的对象引用。如果多个地方需要使用相同的对象,建议使用具名对象。

匿名对象 是一种不具名的临时对象,通常在函数返回值、参数传递和临时计算时使用。

生命周期非常短,它们在表达式结束后立即销毁,析构函数会自动调用。

减少了冗余对象的创建,有助于简化代码并优化性能。

对象拷贝时的编译器优化

● 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷贝。

● 如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。

class A
{
	
public :
	A(int a = 0)
		: _a1(a)
	{
		cout << "A(int a)" << endl;
	} 
	A
	(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		} 
		return* this;
	} 
	~A()
	{
		cout << "~A()" << endl;
	}
private: 
	int _a1 = 1;
};
void f1(A aa)
{}
int main()
{
	//优化
	A aa1=1;

	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;

	// 隐式类型,连续构造+拷⻉构造->优化为直接构造
	f1(1);

	// ⼀个表达式中,连续构造+拷⻉构造->优化为⼀个构造
	f1(A(2));
	cout << endl; 
	return 0;
}

1. 返回值优化
当函数返回一个对象时,编译器可以直接在调用方的内存空间中构造返回的对象,而不是先在函数内部构造,然后再拷贝到调用方。这种优化可以避免一次不必要的对象拷贝。

示例:  

MyClass createObject() {
    return MyClass();
}

在没有RVO时,MyClass() 会先在函数内部创建,然后拷贝到调用方。开启RVO后,MyClass 可以直接在调用方的内存空间中构造。

2.移动语义
在C++11及之后的版本中,移动语义是一种用于减少不必要拷贝的优化。当对象被移动时,编译器会通过“偷取”资源的方式来避免深拷贝。

移动构造函数和移动赋值运算符允许编译器从源对象中“移动”资源,而不是复制它们。这在临时对象或对象需要从另一个作用域转移时特别有用。

示例:

MyClass obj1;
MyClass obj2 = std::move(obj1); // 通过移动语义“偷取”资源,而不是拷贝

3. 拷贝省略
C++17引入了强制的拷贝省略规则,意味着在某些情况下,即使没有定义移动构造函数或移动赋值运算符,编译器也会跳过拷贝操作,直接构造目标对象。

示例:
 

MyClass createObject() {
    MyClass obj;
    return obj; // 编译器可以省略拷贝或移动
}

4. 内联优化(Inline Expansion)
对于小的、频繁调用的函数,编译器可能会选择内联展开函数代码。这样不仅减少了函数调用的开销,同时还可能使编译器在内联的上下文中进行更多的对象优化。

示例:

inline MyClass createObject() {
    return MyClass();
}

5. 对象合并与内存重用
对象合并是一种编译器优化,它尝试将多个对象的生命周期进行分析,如果它们不会同时存在于内存中,编译器可以将它们分配在同一块内存空间中,从而减少内存占用。

6. 懒惰拷贝
也称为写时拷贝(Copy on Write, COW)。当多个对象引用同一资源时,编译器会延迟执行真正的拷贝,直到有一个对象尝试修改该资源时才进行拷贝。这避免了不必要的深拷贝。

7. 循环展开与向量化优化
在对象拷贝的循环中,编译器可能会进行循环展开向量化优化,将循环中的多个对象拷贝操作合并或并行化,以提高性能。

这些优化大部分依赖于编译器本身的优化级别设置(如-O2, -O3),程序员也可以通过编写合理的代码和使用现代C++特性(如移动语义)来帮助编译器进行更好的优化。


网站公告

今日签到

点亮在社区的每一天
去签到