C++是一种功能强大的编程语言,它拥有丰富的特性集合,使得我们可以编写出高效、可维护且性能卓越的代码。其高级概念包括运算符重载、静态成员、友元函数、匿名对象和嵌套类。这些概念在面向对象编程中扮演着至关重要的角色,它们提供了对对象行为和相互作用的灵活控制。为了更好地解释构成C++类的各个部分,我将创建一个日期类作为示例。
class Date
{
public://公有,声明函数
void Print();
private://私有,保存对象的数据
int _year;
int _month;
int _day;
};
在开始之前,我们需要设计一个函数用获得一年中某个月的天数,用来方便我们进行进行日期的计算
int Date::getMonthdays(const int year, const int month)
{
int months[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400) == 0))
{
return 29;
}
return months[month];
}
运算符重载
在C++中,运算符重载允许程序员定义现有运算符的新行为,特别是对于用户定义的数据类型。要重载一个运算符,需要在类或结构体内部声明一个特殊的运算符函数。例如,如果想要重载加法运算符`+`,可以在类中定义如下函数:
returnType operator+(argumentType)
这里,returnType是函数的返回类型,operator是关键字,+是要重载的运算符,而argumentType是传递给函数的参数类型。这种方法使得代码更直观,易于理解,同时也保持了操作的一致性和直观性。
+,+=
我们现在有一个表示日期的类,你可能希望使用加号运算符(`+`)来实现日期对象和整形数据的相加。
Date d1(1970, 1, 1);//构造d1
Date d2 = d1 + 100;
d1 += 100;
我们想要实现+,+=,但是我们要知道他们的区别,+号并不会改变原本的d1的值,而是会返回一个与d1对象一样的,但日期已经加过的值+=则是会改变原本的值,也就是说,他们的返回值一个是拷贝计算后的值,一个是本体!
//Date Date::operator+(int day)//我们设计一个日期对象计算后返回
//{
// Date tmp(*this);
//
// tmp._day += day;
// while (tmp._day > GetMonthDay(tmp._year, tmp._month))
// {
// tmp._day -= GetMonthDay(tmp._year, tmp._month);
// tmp._month++;
// if (tmp._month == 13)
// {
// ++tmp._year;
// tmp._month = 1;
// }
// }
//
// return tmp;
//}
Date Date::operator+(int day) const//但也可以减少拷贝对象,调用+=的实现
{
Date temp = *this;
return temp += day;
}
Date& Date::operator+=(int day)
{
if (day < 0)//因为+可以进行+负数运算,我们可以单独实现后再这里复用函数
{
*this -= -day;
return *this;
}
_day += day;
while (_day > getMonthdays(_year, _month))
{
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
_day -= getMonthdays(_year, _month);
}
return *this;
}
前置++与后置++
前置++返回是已经加过的值
Date& Date::operator++()//也就是加一,我们可以直接调用+=的函数
{
(*this) += 1;
return *this;
}
后置++ 返回时没有加的值,但是本体已经改变
Date Date::operator++(int)
{
Date temp = *this;
(*this) += 1;
return temp;
}
他们因为运算符一致,所以后置++函数参数用int进行区别
比较运算符重载
Date d1(1970, 1, 1);
Date d2(1999, 1, 1);
d2 < d1;
这些运算符通常返回布尔值(bool
类型),表示比较的结果是真(true
)还是假(false
)
bool Date::operator<(const Date& d) const
{
return _year < d._year
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day);
}
const的作用是因为我们比较并不需要改变操作值的大小,而操作值可能是const对象,无法改变,防止错误,对于一边=般不会改变操作值的函数,我们可以对其加上const防止我们代码失误改变大小后及时发现!
<< >> 重载
我们同样可以将“流提取运算符”和“流插入运算符”重载
ostream& operator<<(ostream& out)
{
out << _year << "\\" << _month << "\\" << _day;
out << endl;
return out;
}
istream& operator>>(istream& in)//不要const
{
in >> _year >> _month >> _day;
return in;
}
但是这样写很不符合我们的使用,我们想要
cout << d1;
cin >> d1;
但是,这个类里面的运算符我们只能
d1 << cout;
d1 >> cin;
d1.operator<<(cout);
d1.operator>>(cin);
这样很不符合我们的使用习惯!
运算符重载不仅限于算术运算符,重载比较运算符还可以赋值运算符、下标运算符等,但是
在C++中,有些运算符是不能被重载的,这主要是因为它们与语言的基本结构和内存模型紧密相关。以下是一些不能被重载的运算符:
- 范围解析运算符(::)
- 三元条件运算符(?:)
- 成员访问运算符(.)
- 成员指针访问运算符(.*)
- 对象大小运算符(sizeof)
- 对象类型运算符(typeid)
- 所有形式的类型转换运算符,包括 static_cast、dynamic_cast、const_cast 和 reinterpret_cast
这些限制确保了C++的某些关键特性不会因为运算符重载而变得不确定或混乱。
运算符重载应该谨慎使用,因为如果没有合理的设计,它可能会使代码变得难以理解和维护。在设计重载运算符时,应确保它们的行为与它们的原始意图相符,以避免混淆。
静态成员
在C++编程领域,静态成员占据着独特的位置。它们是类成员,被类的所有实例共享。与每个对象都独有的非静态成员不同,静态成员无论创建多少个类的对象,都作为单一副本存在。
静态数据成员
静态数据成员是属于类而不是任何对象的变量。这意味着即使类的所有对象都被销毁,它们也保留它们的值。它们特别适用于存储需要所有对象访问的类范围信息。
例如,考虑一个表示3D盒子的`Box`类。如果我们想要跟踪创建了多少个`Box`对象,我们可以使用一个静态数据成员来存储计数。这里是一个简化的例子:
class Box {
public:
static int objectCount;
// 构造函数增加对象计数
Box() {
objectCount++;
}
};
// 初始化静态成员
int Box::objectCount = 0;
在这个例子中,`objectCount`在所有`Box`实例中共享,并且每次创建新的`Box`时都会递增。
静态成员函数
另一方面,静态成员函数可以在没有类的对象的情况下被调用。它们只能访问静态数据成员和其他静态成员函数。这些函数不绑定到任何特定对象,因此没有`this`指针。
静态成员函数可以用来访问`objectCount`静态数据成员,例如:
class Box {
public:
static int objectCount;
// 静态函数访问objectCount
static int getObjectCount() {
return objectCount;
}
};
int Box::objectCount = 0;
使用和考虑
他们可以解决一个经典问题,计算从1+到n,不能使用循环,迭代......我们可以通过查看静态变量,每当调用一次静态变量,就加一,sum记录加的总数,就可以解决这个问题
当你需要维护对类的所有对象共同的状态或行为时,使用静态成员可以使代码更高效。然而,重要的是要谨慎使用它们。过度使用静态成员会使你的代码变得不够灵活,更难以测试。
此外,静态成员在程序执行开始之前就初始化为零或构造,在进入`main()`之前。这有时会导致所谓的“静态初始化顺序混乱”,即跨翻译单元的静态变量初始化顺序是未定义的。总之,静态成员是C++的一个强大功能,正确使用时,可以帮助以高效的方式管理共享数据和功能。
友元
友元函数
在C++编程领域,友元函数的概念是一个有趣的特性,它允许函数从类的外部访问类的私有和受保护成员。这一能力在我们需要允许某些非成员函数访问类的私有数据而不破坏封装和数据隐藏原则的情况下特别有用。
C++中的友元函数是一种虽然不是类的成员,但被授予访问类的私有和受保护成员的权限的函数。它在类定义中用`friend`关键字声明,其原型出现在类中,但其定义在外部。
要声明友元函数,你只需在类的私有成员需要访问的类中,用`friend`关键字预先声明函数原型。以下是一个简单的例子:
class MyClass {
private:
int secretValue;
public:
friend void friendFunction(MyClass &obj);
};
void friendFunction(MyClass &obj) {
// 访问MyClass的私有成员
std::cout << "Secret Value is: " << obj.secretValue << std::endl;
}
我们也可以通过友元函数来重载cout cin函数
//内联函数
inline ostream& operator<<(ostream& out, const Date& d)//减少拷贝
{
out << d._year << "\\" << d._month << "\\" << d._day;
out << endl;
return out;
}
inline istream& operator>>(istream& in, Date& d)//不要const
{
in >> d._year >> d._month >> d._day;
return in;
}
我们就可以这样调用了
cout << d1;
cin >> d1;
友元函数的好处有:
1. 灵活性:友元函数提供了一个替代成员函数访问私有数据的方法,这使得设计变得更加灵活。有时候,某些操作可能更自然地作为一个独立的函数存在,而不是作为一个类的成员函数。
2. 效率:与成员函数相比,友元函数可以更高效。因为它们不需要通过对象来访问私有数据,避免了成员函数调用的开销,从而提高了执行效率。
3. 实用性:特别是在操作符重载方面,友元函数非常有用。有时,某些操作需要访问不同类的私有数据,而这些数据不能通过成员函数访问。
考虑到最佳实践:
- 保持封装:应该谨慎使用友元函数,以保持面向对象编程的封装性。友元函数应该仅在必要时使用,以防止破坏类的封装性。
- 限制访问:应该仅授予必须访问类敏感数据的函数友元状态。这样可以最小化友元函数对类的影响,同时保持类的封装性。
- 文档化:在代码中清晰地记录授予友元访问权限的原因是很重要的。这样做可以帮助保持代码的清晰性,并使其他开发人员更容易理解友元函数的用途。
C++中的友元函数提供了一个受控的封装突破口,允许外部函数与私有类数据交互。当适当使用时,它们可以导致更高效和灵活的代码。然而,关键是要谨慎使用这一功能,以保持面向对象设计原则的完整性。
友元类
友元类的声明相当直接。在共享其成员的类内部,通过`friend`关键字后跟类名来声明友元类。以下是一个简单的例子来说明这一点:
class ClassA {
private:
int data;
public:
ClassA(int value) : data(value) {}
friend class ClassB; // 声明ClassB为友元类
};
class ClassB {
public:
void showData(ClassA& a) {
// 访问ClassA的私有数据
std::cout << "ClassA data: " << a.data << std::endl;
}
};
在上面的例子中,ClassB是ClassA的朋友,这意味着它可以访问ClassA的私有成员data。但是,ClassA不能访问ClassB!这是一个强大的功能,但应谨慎使用。友元类的使用可能会破坏封装,这是面向对象编程的核心原则。因此,重要的是只有在绝对必要时,以及当其他设计模式或技术不足以满足需求时,才使用这个功能。
友元类常见的使用场景之一是在实现复杂数据类型的操作符重载时。有时,操作函数需要访问类的私有部分以执行其操作,声明操作函数为友元允许这种访问。
值得注意的是,友元关系不是传递的或继承的。如果ClassC是ClassB的朋友,而ClassB是ClassA的朋友,ClassB并不自动成为ClassA的朋友。C++中友元类和函数的概念证明了该语言的灵活性及其满足广泛编程需求的能力。但同时破坏了C++的私有有封装,使用友元的时候一定要注意!
匿名对象
匿名对象通常用于即时的一次性操作,其中创建对象,使用后即被丢弃,无需持久的引用。这在只需要对象来调用成员函数或作为另一个函数的参数传递的场景中特别有用。
在C++中创建匿名对象非常直接。它涉及实例化一个类而不将结果对象分配给变量。例如:
class MyClass {
public:
MyClass() { /* 构造函数代码 */ }
void display() const { /* 显示一些内容 */ }
};
// 创建一个匿名对象并调用成员函数
MyClass().display();
在上面的代码片段中,`MyClass().display();` 创建了一个`MyClass`的匿名对象,并立即调用它的`display`方法。该语句执行后,匿名对象超出作用域并被销毁。
使用匿名对象的主要优点之一是减少内存消耗,因为这些对象是临时的,不会超出它们使用的范围。它们还通过消除在不需要后续引用的情况下命名变量的需要,简化了代码。
然而,重要的是要注意匿名对象有其局限性。由于它们没有分配给变量,它们在初始使用后不能被访问。这意味着除非已经使用或存储在其他地方,否则对象内的任何状态更改或信息都会丢失。
在实践中,可以在各种应用中看到匿名对象,例如在操作符重载中,它们在操作期间作为值的临时载体。它们在函数调用中也很有用,在这些调用中需要对象作为参数,但之后不再需要。
class MyClass {
public:
MyClass(int a) { /* 构造函数代码 */ }
void display() const { /* 显示一些内容 */ }
int A()
{
return 2;
}
};
MyClass fun()
{
int b=MyClass().A();
return MyClass(b);
}
内部类
嵌套类
C++中的嵌套类可以被视为一种将类声明以逻辑和可管理的方式分组的方法。它们允许在类中定义类,这可以导致更可读和可维护的代码。内部类可以访问外部类的私有和受保护成员,作用有点像友元类,但重要的是要注意,这种关系不是相互的;外部类没有特殊的访问内部类的私有成员的权限。
语法和可访问性
定义嵌套类时,语法很直接。内部类在外部类的主体内声明。它可以在公共、受保护或私有部分声明,这取决于所需的可访问性级别。如果内部类在公共部分声明,可以使用作用域解析运算符实例化,如此:`OuterClass::InnerClass obj;`。然而,如果它在私有部分声明,则不能被外部代码直接实例化,这对于某些设计模式可能很有用。
class OuterClass {
int outerData;
class InnerClass {
public:
void innerFunction(OuterClass& outer) {
outer.outerData = 100; // 访问OuterClass的私有成员
}
};
public:
void outerFunction() {
InnerClass inner;
inner.innerFunction(*this);
}
};
int main() {
OuterClass outer;
outer.outerFunction();
return 0;
}
在这个例子中,InnerClass是OuterClass内的一个嵌套类,并且有一个公共函数innerFunction,它接受一个OuterClass对象的引用。这个函数可以修改OuterClass的私有数据,展示了两个类之间的紧密关系。