类中私有和保护
在面向对象编程中,封装是一种将数据(变量)和操作数据的函数结合在一起的机制。数据(或属性)被保护起来,不能被外部函数直接访问,只能通过类中定义的函数(方法)来访问。
类的成员可以设为以下三种访问修饰符之一:
1. 公有(public): 可以被任何其他函数或方法访问。
2. 私有(private): 只能被类中的其他成员(方法或者成员函数)访问,不能被派生的类访问。
3. 保护(protected): 只能被类中的其他成员或派生类的成员访问,不能被类的对象访问。
私有成员和保护成员的主要目的是数据隐藏。这有助于保证类的内部数据不会被外部代码错误地访问或修改。保护成员在派生类中可以访问,这在某些情况下是有用的,因为它可以为派生类提供对基类数据成员的直接访问,同时不允许代码外部进行访问。
友元
友元可以使一个函数或者类访问另一类中私有(private)或者保护(protected)的成员变量和成员函数。
友元全局函数
定义在类外部的函数,可以被设置为类的友元函数,此时这个函数就可以直接访问类的私有和保护成员。
class A {
private:
int value;
public:
A(int n) { value = n; }
friend void showA(A); // 声明友元函数
};
void showA(A a) {
std::cout << a.value << std::endl; // 可以访问私有成员
}
int main() {
A a(10);
showA(a);
return 0;
}
友元类
一个类可以被声明为另一个类的友元类。在友元类中,它的所有成员函数都可以访问另一个类的私有和保护成员。
友元类的注意事项:
- 友元类不能被继承
- 友元关系是单向的,不具备交换性。
- 若类B是类A的友元,类A不一定是类B的友元。B是类A的友元,类C是B的友元,类C不一定是类A的友元,要看类中是否有相应的声明。
class B; // B是一个前置声明
class A {
private:
int value;
public:
A(int n) { value = n; }
friend class B; // 声明B为A的友元类
};
class B {
public:
void showA(A a) {
std::cout << a.value << std::endl; // 可以访问私有成员
}
};
3. 友元成员函数
一个类的成员函数可以被声明为另一个类的友元函数,这样就可以访问该类的私有和保护成员。
class B;
class A {
private:
int value;
public:
A(int n) { value = n; }
friend void B::showA(A a); // 声明B的成员函数为A的友元函数
};
class B {
public:
void showA(A a) {
std::cout << a.value << std::endl; // 可以访问私有成员
}
};
运算符重载
运算符重载是一种在C++中的特殊功能,允许程序员自定义用户定义类型(例如类和结构)的运算符行为。这意味着你可以改变已存在运算符的功能,使它们能用于你自己定义的类型,例如类。例如,你可能希望使用"+"运算符将两个你自己定义的对象相加,这是默认情况下不可能的,但你可以通过运算符重载来实现。
运算符重载通常是通过在你的类中定义一个函数来实现的,函数名是operator关键字后跟你想重载的运算符。例如,如果你想重载"+"运算符,你可能会定义一个名为"operator+"的函数。
以下是一个重载向量加法的示例。在这个例子中,我们假设向量是由两个元素组成的。
class Vector2D {
public:
int x, y;
Vector2D(int x, int y): x(x), y(y) {}
// 重载 "+" 运算符
Vector2D operator+(const Vector2D& v) {
return Vector2D(this->x + v.x, this->y + v.y);
}
};
在这个例子中,"+"运算符已经被重载,以便它可以用于Vector2D对象。这样,你就可以用自然的语法把两个向量加在一起了:
Vector2D v1(1, 2);
Vector2D v2(3, 4);
Vector2D v3 = v1 + v2; // 现在这是合法的
下面是个简单的例子,重载了减法、乘法、以及"<<"运算符来打印Vector对象
#include <iostream>
using namespace std;
class Vector2D {
public:
int x, y;
Vector2D(int x, int y) {
this->x = x;
this->y = y;
}
// 重载 "+" 运算符
Vector2D operator+(const Vector2D& v) {
return Vector2D(this->x + v.x, this->y + v.y);
}
// 重载 "-" 运算符
Vector2D operator-(const Vector2D& v) {
return Vector2D(this->x - v.x, this->y - v.y);
}
// 重载 "*" 运算符
Vector2D operator*(int factor) {
return Vector2D(this->x * factor, this->y * factor);
}
// 重载 "<<" 运算符。注意,这需要是一个友元函数,因为它的左操作数是std::ostream对象
friend std::ostream& operator<<(std::ostream& os, const Vector2D& v);
};
// 实现 "<<" 运算符
ostream& operator<<(ostream& os, const Vector2D& v) {
os << '(' << v.x << ", " << v.y << ')';
return os;
}
int main() {
Vector2D v1(1, 2);
Vector2D v2(3, 4);
Vector2D v3 = v1 + v2; // 这是合法的
Vector2D v4 = v1 - v2; // 这也是合法的
Vector2D v5 = v1 * 3; // 这也是合法的
std::cout << "v3: " << v3 << std::endl;
std::cout << "v4: " << v4 << std::endl;
std::cout << "v5: " << v5 << std::endl;
return 0;
}
重载关系运算符
关系运算符有六种:==
, !=
, <
, >
, <=
, >=
可以使用非成员函数和成员函数两种版本,建议使用成员函数。
#include <iostream>
using namespace std;
class Point
{
public:
int x, y;
Point(int x, int y) {
this->x = x;
this->y = y;
}
//重载 ==
bool operator ==(const Point &p) const
{
return x == p.x && y == p.y;
}
//重载 !=
bool operator != (const Point &p) const
{
return !(*this == p);
}
//重载 <
bool operator < (const Point &p) const
{
return x < p.x || (x == p.x && y < p.y);
}
//重载 >
bool operator > (const Point &p) const
{
return !(*this < p);
}
//重载 <=
bool operator <= (const Point &p) const
{
return (*this < p) || (*this == p);
}
//重载 >=
bool operator >= (const Point &p) const
{
return (*this > p) || (*this == p);
}
};
-
当你重载
==
运算符时,你也应该重载!=
运算符,因为它们的结果是对立的。同样地,如果你重载了<
或>
运算符,你应该考虑重载<=
或>=
运算符。 -
重载关系运算符的结果应该是
bool
类型。 -
在某些情况下,重载关系运算符可能并不直观,比如对于复数或者矩阵。在这些情况下,可能需要更明确地定义"大小"的概念。
重载左移运算符
重载左移运算符(<<
)用于输出自定义对象的成员变量,在实际开发中很有价值(调试和日志)。
只能使用非成员函数版本。
由于它通常需要访问对象的私有成员,所以通常被声明为类的友元函数。
#include <iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y = 0) : x(x), y(y) {}
friend ostream& operator<<(ostream& os, const Point& p);
private:
int x;
int y;
};
ostream& operator<<(ostream& os, const Point& p)
{
os << "(" << p.x << "," << p.y << ")";
return os;
}
int main() {
Point p1(1, 2);
cout << p1 << std::endl; // 输出 "(1, 2)"
return 0;
}
注意点:
- 重载的
<<
运算符必须是个全局函数,也就是说不能是某个类的成员函数。这是因为如果它是一个类的成员函数,那么它的左操作数就必须是一个该类的对象,而不是我们想要的:我们想要的是让<<
运算符的左操作数是一个ostream
对象比如(比如cout) - 重载的
<<
运算符应该返回ostream
,以支持链式调用,比如cout << p1 << p2;
重载下标运算符
如果对象中有数组,重载下标运算符[]
,操作对象中的数组将像操作普通数组一样方便。
下标运算符必须以成员函数的形式进行重载。
下标运算符重载函数的语法:``返回值类型 &perator;`
或者:
const 返回值类型 &operator[](参数) const;
使用第一种声明方式,[]不仅可以访问数组元素,还可以修改数组元素。
使用第二种声明方式,[]只能访问而不能修改数组元素。
在实际开发中,我们应该同时提供以上两种形式,这样做是为了适应const对象,因为通过const 对象只能调用const成员函数,如果不提供第二种形式,那么将无法访问const对象的任何数组元素。
在重载函数中,可以对下标做合法性检查,防止数组越界。
#include<iostream>
using namespace std;
class MyArray
{
private:
int size;
int* data;
public:
MyArray(int size): size(size),data(new int[size]){}
~MyArray() { delete[]data; }
int& operator[](int index)
{
if(index<0|| index>=size)
{
cout<<"数组越界"<<endl;
exit(0);
}
return data[index];
}
const int& operator[](int index) const
{
if (index < 0 || index >= size)
{
cout << "数组越界" << endl;
exit(0);
}
return data[index];
}
};
int main() {
MyArray a(10);
a[0] = 1;
cout << a[0] << std::endl; // 输出 1
int outOfRangeValue = a[100]; // 索引超出范围,返回特殊值
return 0;
}
注意:
- 检查下标的合法性,防止数组越界
- 应该同时提供非
const
版本和const
版本的重载,以支持const
对象 []
的返回值应该是元素的引用,这样就可以修改元素的值const
版本的重载的返回值应该是const
引用,这样就可以防止通过const
对象修改元素的值。
重载赋值运算符
在讲解这个概念之前,首先要明确几个自动由编译器提供的函数:默认构造函数、析构函数、拷贝构造函数和赋值运算符。如果你没有在类中显式地定义这些函数,编译器就会自动为你提供。
然而,编译器提供的这些默认函数可能并不能满足所有的需求。比如,如果你的类中有指针成员,那么就需要自己重载赋值运算符,来实现深拷贝,防止浅拷贝带来的问题。
现在,我们以一个具体的例子来讲解。假设我们有一个简单的类,包含一个字符串指针作为成员。
class MyClass {
char* str;
public:
MyClass() : str(new char[50]) { }
~MyClass() { delete[] str; }
};
在这个例子中,如果我们没有重载赋值运算符,编译器会提供一个默认的赋值运算符,实现浅拷贝。但是,这会引发一个问题。比如:
MyClass obj1;
MyClass obj2 = obj1; // 调用默认赋值运算符
在这种情况下,obj1
和obj2
的str
指向同一个内存空间,当其中一个对象被销毁时,会释放这个内存空间。然后,当另一个对象再次被销毁时,就会尝试释放已经被释放的内存空间,导致未定义的行为。
所以,我们需要重载赋值运算符,来进行深拷贝:
MyClass& MyClass::operator=(const MyClass& source) {
if (this == &source) // 检查自赋值
return *this;
delete[] str; // 删除原有内存
str = new char[50]; // 申请新内存
strcpy(str, source.str); // 复制数据
return *this; // 返回自身的引用
}
在这个版本的赋值运算符中,我们首先检查自赋值,然后释放原有的内存,申请新的内存,并复制数据。这样,每个对象都有自己的内存空间,避免了浅拷贝的问题。
赋值运算符和拷贝构造函数的主要区别在于,拷贝构造函数是用来创建新的对象的,而赋值运算符是用来将一个已经存在的对象的状态赋给另一个已经存在的对象。
内存池
内存池(Memory Pool),也称作对象池,是一种内存管理技术。在编程中,对内存的分配和释放会消耗相当大的资源。特别是在需要大量小块内存的场景下,频繁内存分配和释放会导致性能下降和内存碎片的问题。内存池技术就是为了解决这种问题。
内存池的基本思想是一次性分配一大块内存,然后将这块内存分割成许多小块,当程序需要小块内存时,就从内存池中获取,不需要时则归还给内存池。这样就避免了频繁调用系统的内存分配函数,提高了内存管理的效率。
内存池的工作过程可以简单归纳为一下几个步骤:
- 预先分配:内存池在初始化时,会预先申请一大块连续的内存空间。
- 内存分割:将预先申请的内存空间分割成多个固定大小或者不同大小的小块。
- 内存分配:当程序请求分配内存时,内存池会查找是否有合适大小的内存块,如果有,则直接返回该内存块的地址,如果没有,内存池会重新分配一块足够大的内存。
- 内存回收:当程序释放内存时,不是直接归还给系统,而是归还给内存池,内存池会将其标记为可用,供以后分配时使用。
内存池技术主要有以下优点:
- 提高性能:内存池大大减少了系统调用的次数,从而提高了性能。
- 减少内存碎片:内存池通过重新使用回收的内存,减少了内存碎片。
- 简化内存管理:内存池提供了一种简单且高效的内存管理方式。
重载new和delete运算符
在C++中,new
和delete
是用来动态分配和释放内存的运算符。通过重载这两个运算符,你可以控制内存的分配和释放方式,从而达到优化内存使用,减少内存碎片等目标。下面我会提供一个简单的重载new
和delete
运算符的示例。
首先,我们需要理解new
和delete
运算符的工作原理。在C++中,当你使用new
运算符时,它实际上做了两件事情:首先,它调用::operator new
函数来分配内存;然后,它调用对象的构造函数来初始化对象。类似地,当你使用delete
运算符时,它也做了两件事情:首先,它调用对象的析构函数来销毁对象;然后,它调用::operator delete
函数来释放内存。
因此,当我们谈论重载new
和delete
运算符时,我们实际上是在谈论重载::operator new
和::operator delete
函数。
class MyClass {
public:
// 重载 ::operator new
static void* operator new(size_t size) {
std::cout << "Custom new for size " << size << std::endl;
void* p = malloc(size); // 分配内存
if (!p) throw std::bad_alloc(); // 分配失败,抛出异常
return p;
}
// 重载 ::operator delete
static void operator delete(void* p) {
std::cout << "Custom delete" << std::endl;
free(p); // 释放内存
}
};
需要注意的是,当你在类中重载这两个函数时,只有这个类的对象才会使用这两个自定义版本的函数。如果你想要所有的对象都使用这两个自定义版本的函数,你可以在全局作用域中重载这两个函数。
在使用这些自定义版本的函数时,需要注意以下几点:
::operator new
函数应该返回一个指针,指向分配的内存。如果内存分配失败,它应该抛出一个std::bad_alloc
异常,或者它的派生异常。::operator delete
函数没有返回值。如果你在这个函数中使用了throw
语句,那么如果在析构函数中也抛出了异常,可能会导致程序终止。- 如果你重载了这两个函数,你可能也需要重载它们的数组版本
::operator new[]
和::operator delete[]
,以保证数组的正确分配和释放。
重载括号运算符
语法:返回值类型 operator()(参数列表)
class Multiply {
public:
// 重载括号运算符
int operator()(int x, int y) {
return x * y;
}
};
int main() {
Multiply multiply;
// 使用重载的括号运算符
int result = multiply(10, 20);
std::cout << "The result is " << result << std::endl;
return 0;
}
注意点:
- 括号运算符必须作为类成员函数重载。不能在类外部重载。
- 你可以为括号运算符提供任意数量的参数,也可以不提供参数。但是,你不能为括号运算符默认参数。
- 当你重载括号运算符时,它的返回类型可以是任意类型,包括
void
。
注意的错误和陷阱:
- 不要在不适当的情况下重载括号运算符。括号运算符通常在仿函数或者与函数调用相关的场景下重载。如果你在其他情况下重载括号运算符,可能会让你的代码变得难以理解。
- 重载运算符不会改变运算符的优先级。括号运算符有最高的优先级,重载后的括号运算符也有最高的优先级。
重载一元运算符
可重载的一元运算符。
1)++
自增 2)--
自减 3)!
逻辑非 4)&
取地址
5)~
二进制反码 6)*
解引用 7)+
一元加 8) -
一元求反
一元运算符通常出现在它们所操作的对象的左边。
但是,自增运算符++和自减运算符–有前置和后置之分。
C++ 规定,重载++或–时,如果重载函数有一个int形参,编译器处理后置表达式时将调用这个重载函数。
成员函数版:CGirl &operator++(); // ++前置
成员函数版:CGirl operator++(int); // 后置++
非成员函数版:CGirl &operator++(CGirl &); // ++前置
非成员函数版:CGirl operator++(CGirl &,int); // 后置++
class Number {
private:
int value;
public:
Number(int n) : value(n) {}
// 前置递增运算符
Number& operator++() {
++value;
return *this;
}
// 前置递减运算符
Number& operator--() {
--value;
return *this;
}
// 后置递增运算符
Number operator++(int) {
Number tmp(*this); // 拷贝原对象
++value;
return tmp; // 返回原值
}
// 后置递减运算符
Number operator--(int) {
Number tmp(*this); // 拷贝原对象
--value;
return tmp; // 返回原值
}
int getValue() const {
return value;
}
};
注意以下几点:
- 你可以通过在类中定义成员函数,或在类外定义非成员函数来重载一元运算符。但通常建议在类中定义成员函数来重载一元运算符。
- 如果你选择在类外定义非成员函数来重载一元运算符,那么你需要给该函数提供一个参数,表示运算符的操作数。
- 你可以为一元运算符指定任何返回类型。但通常,前置递增和递减运算符应该返回操作数的引用,后置递增和递减运算符应该返回操作数的值。