admin管理员组文章数量:1794759
类和对象(万字总结!深度总结了类的相关知识)(下)
1. 再谈构造函数
函数体内赋值
在构造函数体内进行赋值,即对象的成员变量先通过默认构造函数创建,随后在构造函数体内被赋值。
代码语言:javascript代码运行次数:0运行复制class Date{
public:
Date(int year, int month, int day){
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
缺点:
- 效率较低:在构造函数体内赋值时,成员变量已经经历了一次默认初始化,之后再进行赋值。这会导致两步操作,特别是对于复杂类型对象,可能导致不必要的性能损耗。
- 无法处理某些成员类型:对于
const
成员、引用类型、以及没有默认构造函数的类成员,无法使用这种方式赋值,必须使用初始化列表。
初始化列表
初始化列表是在构造函数的声明后,紧跟着冒号 :
的一部分。它在对象创建时,直接调用成员变量的构造函数或对其进行初始化。
class Date{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
【注意】
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
-
const
成员变量 - 自定义类型成员(且该类没有默认构造函数时)
class A{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B{
public:
// 初始化列表:对象的成员定义的位置
B(int a, int& ref)
: _aobj(a)
, _ref(ref)
, _n(10)
{}
private:
A _aobj; // 没有默认构造函数
// 特征:必须在定义的时候初始化
int& _ref; // 引用
const int _n; // const
};
【注意】
在B类的初始化列表中,为什么使用int& ref
而不是int ref
,原因是ref如果是局部变量,那么出了作用域就销毁了,此时_ref
就相当于野引用,所以这里应该是int&
。
优点:
- 效率高:初始化列表直接在对象创建时初始化成员变量,避免了先默认构造再赋值的额外步骤。
- 强制初始化:某些类型(如
const
和引用
)必须通过初始化列表进行初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序
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();
return 0;
}
// A. 输出1 1
// B. 程序崩溃
// C. 编译不通过
// D. 输出1 随机值
2. Static
静态成员
静态成员变量
静态成员变量(也称为类变量)是指在面向对象编程中,属于类而不是某个特定对象的变量。它的特性是在类的所有实例之间共享,即无论创建了多少个对象,静态成员变量在内存中只有一个副本,所有实例对这个变量的修改都会反映在所有其他实例中。
- 属于类本身:静态成员变量是类级别的,不能通过对象直接定义,而是通过类定义。
- 共享性:所有对象共享同一个静态成员变量,修改这个变量时,所有的实例都会感知到修改的值。
- 生命周期长:静态成员变量随着类的加载而存在,类卸载时才会消失。
- 访问方式:可以通过类名直接访问,而不需要实例化对象
#include<iostream>
using namespace std;
class MyClass {
public:
static int staticVar;
void display() {
cout << "Static Variable: " << staticVar << endl;
}
};
int MyClass::staticVar = 10; // 初始化静态成员变量
int main() {
MyClass obj1, obj2;
obj1.display(); // 输出:Static Variable: 10
obj2.display(); // 输出:Static Variable: 10
obj1.staticVar = 20; // 修改静态变量
obj1.display(); // 输出:Static Variable: 20
obj2.display(); // 输出:Static Variable: 20
return 0;
}
在这个例子中,staticVar
是MyClass
的静态成员变量。即使obj1
和obj2
是不同的实例,但它们都共享同一个staticVar
变量。当obj1
修改了staticVar
的值,obj2
也会看到同样的变化。
【注意】
- 静态成员变量的初始化必须在类定义外进行。
- 不能通过对象直接初始化静态成员变量。
静态成员函数
静态成员函数是与类相关联的函数,而不是与类的具体实例关联。它属于类本身,而不是类的某个对象。静态成员函数在使用时无需实例化对象,可以直接通过类名调用。不依赖对象:静态成员函数是类级别的函数,不依赖于类的具体对象。它可以在没有实例化类对象的情况下直接调用。
- 不能访问非静态成员变量和非静态成员函数:由于静态成员函数不依赖于对象,它不能直接访问类的非静态成员变量或非静态成员函数,因为这些成员变量和成员函数是依赖于具体对象的。
- 可以访问静态成员变量:静态成员函数可以访问静态成员变量,因为静态成员变量同样是类级别的,与对象无关。
- 常用于工具函数或与实例无关的逻辑:静态成员函数常用于执行与具体对象无关的任务,比如全局计数、工具函数等。
#include<iostream>
using namespace std;
class MyClass {
public:
static int staticVar; // 静态成员变量
// 静态成员函数
static void staticFunction() {
cout << "Static Variable: " << staticVar << endl;
}
// 非静态成员函数
void nonStaticFunction() {
cout << "Non-static function can access staticVar: " << staticVar << endl;
}
};
int MyClass::staticVar = 10; // 初始化静态成员变量
int main() {
// 调用静态成员函数,不需要创建对象
MyClass::staticFunction(); // 输出:Static Variable: 10
// 修改静态成员变量的值
MyClass::staticVar = 20;
MyClass::staticFunction(); // 输出:Static Variable: 20
// 创建对象,调用非静态成员函数
MyClass obj;
obj.nonStaticFunction(); // 输出:Non-static function can access staticVar: 20
return 0;
}
【解释】
- 静态成员变量
staticVar
:这是一个静态成员变量,属于整个类。无论创建多少个对象,它的值在所有对象间是共享的。 - 静态成员函数
staticFunction
:可以通过类名MyClass::staticFunction()
调用,无需创建对象。它能访问静态成员变量staticVar
。 - 非静态成员函数
nonStaticFunction
:它可以访问静态成员变量staticVar
,因为静态成员变量对整个类可见。
【使用静态成员函数的场景】
- 与对象无关的操作:当函数的逻辑不依赖具体的对象时,可以使用静态成员函数,例如工具类中的数学计算方法。
- 访问或操作静态成员变量:静态成员函数常用于操作静态成员变量,例如维护类实例的全局计数等。
- 工厂模式:静态成员函数可以用于返回类的实例,如工厂模式中常用的“创建对象”的函数。
【注意事项】
- 静态成员函数无法直接调用非静态成员变量和非静态成员函数。如果需要访问,必须传递对象实例或者将非静态成员变量变为静态成员变量。
- 静态成员函数虽然不依赖于对象,但是它们同样遵守类的访问控制(如
private
、protected
)。
3. 友元
友元的本质: 友元打破了 C++ 封装的严格限制,使得指定的外部函数或类能够访问类的私有成员和保护成员。友元并不是类的成员,它是一种特殊的外部“访问权限声明”。
友元的类型:
- 友元函数:普通的函数可以通过在类内声明为友元,从而可以访问该类的私有和保护成员。
- 友元类:一个类可以将另一个类声明为友元,这样友元类的所有成员函数都能访问该类的私有和保护成员。
- 友元成员函数:某类的特定成员函数可以被声明为友元,只对该特定函数开放访问权限。
友元的应用场景:
- 操作符重载:特别是像
<<
和>>
这样的输入输出运算符重载,通常需要通过友元函数来访问类的私有数据。 - 调试和日志:通过友元,某些调试类可以直接访问目标类的内部状态,用于日志记录或状态检查。
示例 - 操作符重载中的友元函数:
代码语言:javascript代码运行次数:0运行复制class Complex {
private:
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
// 声明友元函数
friend std::ostream& operator<<(std::ostream& out, const Complex& c);
};
// 友元函数定义
std::ostream& operator<<(std::ostream& out, const Complex& c) {
out << c.real << " + " << c.imag << "i";
return out;
}
在这个例子中,<<
运算符被声明为 Complex
类的友元函数,从而能够访问 Complex
的私有成员 real
和 imag
。
4. 内部类
内部类的本质: 内部类是一个类的成员,存在于另一个类的定义内部。它与外部类存在某种逻辑关系,但不会自动继承访问权限。通常,内部类用于表示外部类的一个组成部分,封装复杂的数据结构或功能。
内部类的访问规则:
- 内部类和外部类之间的访问权限是独立的,除非明确声明为友元。
- 外部类不能直接访问内部类的私有成员,反之亦然。
- 内部类可以访问外部类的公有和保护成员。
内部类的应用场景:
- 实现细节封装:内部类经常用来封装外部类的实现细节,隐藏复杂的内部逻辑。
- 模块化设计:内部类可以用于实现更清晰的模块化设计,将一个大类拆分成多个小的内部类来管理不同的功能。
示例 - 内部类与外部类的交互:
代码语言:javascript代码运行次数:0运行复制class Outer {
private:
int outerValue;
public:
Outer(int val) : outerValue(val) {}
// 内部类声明
class Inner {
public:
void displayOuter(Outer& o) {
std::cout << "Outer class value: " << o.outerValue << std::endl; // 访问外部类的私有成员
}
};
};
在这个例子中,Inner
类是 Outer
类的内部类,Inner
类的 displayOuter
函数可以访问 Outer
类的私有成员。
5. 匿名对象
匿名对象的本质: 匿名对象是未被命名的对象,它通常是在表达式中临时生成的,生命周期极短。匿名对象常见于临时对象的创建和函数返回值中。匿名对象的好处是避免了不必要的命名和生命周期管理,简化了代码逻辑。
匿名对象的特点:
- 自动销毁:匿名对象在使用完后立即销毁,不占用额外的资源。
- 适用于短期操作:非常适合在函数调用中返回临时对象,避免了拷贝和对象管理的复杂性。
匿名对象的应用场景:
- 临时计算结果:某些场景下,使用匿名对象来计算临时结果非常常见。
- 返回值优化:在函数返回值时,匿名对象与返回值优化(RVO)结合,能有效减少拷贝。
示例 - 匿名对象作为返回值:
代码语言:javascript代码运行次数:0运行复制class Example {
public:
Example() {
std::cout << "Constructor called!" << std::endl;
}
~Example() {
std::cout << "Destructor called!" << std::endl;
}
};
Example createExample() {
return Example(); // 返回匿名对象
}
int main() {
Example e = createExample(); // 用匿名对象初始化 e
}
在这里,Example()
是匿名对象,它的生命周期仅限于函数调用,它的构造和析构顺序也表明了其生命周期的短暂性。
6. 拷贝对象时的一些编译器优化
编译器在处理对象拷贝时,会进行一些常见的优化以提高性能。以下是几种主要的优化技术:
返回值优化(RVO
)
RVO
是一种编译器优化,它避免了在函数返回时临时对象的拷贝构造。编译器在函数返回时直接在目标位置创建对象,消除了拷贝的开销。
示例:
代码语言:javascript代码运行次数:0运行复制class A {
public:
A() { std::cout << "Constructor" << std::endl; }
A(const A&) { std::cout << "Copy Constructor" << std::endl; }
};
A createA() {
return A(); // RVO,避免拷贝
}
int main() {
A a = createA(); // RVO 使得没有调用拷贝构造函数
}
移动语义
C++11 引入了移动语义,通过移动构造函数和移动赋值运算符,能够有效避免深拷贝的开销。移动语义将对象资源的所有权转移,而不是进行拷贝。
示例:
代码语言:javascript代码运行次数:0运行复制class B {
public:
B() { std::cout << "Constructor" << std::endl; }
B(B&&) { std::cout << "Move Constructor" << std::endl; }
};
B createB() {
return B(); // 移动构造
}
此例中,移动语义会避免不必要的深拷贝,大大提升性能。
拷贝省略
在某些情况下,C++ 标准允许编译器跳过某些不必要的拷贝操作,比如在函数返回时,编译器直接在调用者的上下文中构造返回对象,避免了临时对象的创建和拷贝。
7. 再次理解封装
封装的本质: 封装是面向对象编程(OOP
)的核心原则之一。它通过将对象的状态(数据)和行为(方法)封装在类中,限制外部对类内部实现的直接访问。封装的目的是保护对象的完整性,并通过控制访问权限实现信息隐藏。
封装的三种访问控制:
- Public(公有):外部可以自由访问,表示开放给外部的接口。
- Private(私有):外部无法访问,只有类的内部成员函数可以访问。
- Protected(保护):子类可以访问,但外部类无法访问。
封装的优势:
- 数据安全性:通过私有和保护成员变量,封装可以保护数据的完整性,避免外部直接修改数据,确保程序的稳定性和安全性。
- 灵活性:通过封装,内部实现可以随时更改,而不影响外部代码,因为外部只能通过公开接口与对象交互。
- 降低耦合:封装可以减少类之间的依赖和耦合,提高代码的可维护性和可扩展性。
示例 - 封装的好处:
代码语言:javascript代码运行次数:0运行复制class BankAccount {
private:
double balance; // 私有数据成员,外部无法直接访问
public:
BankAccount(double initBalance) : balance(initBalance) {}
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
}
}
double getBalance() const {
return balance;
}
};
通过封装,balance
变量不会被外部代码直接修改,外部只能通过 deposit
和 withdraw
函数来
本文标签: 类和对象(万字总结!深度总结了类的相关知识)(下)
版权声明:本文标题:类和对象(万字总结!深度总结了类的相关知识)(下) 内容由林淑君副主任自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.xiehuijuan.com/baike/1754806463a1706680.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论