C++面对对象

Channing Hsu

一、面向对象的三大特性

1. 封装(Encapsulation)

  • 定义:将数据操作数据的方法封装在一个类中,防止外部干扰和不正确的访问。

  • 功能:保护数据完整性,对外提供接口。

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Person {
    private:
    std::string name;
    int age;

    public:
    void setName(const std::string& n) { name = n; }
    void setAge(int a) { age = a; }
    std::string getName() const { return name; }
    int getAge() const { return age; }
    };

2. 继承(Inheritance)

  • 定义:让一个类获得另一个类的属性和方法,实现代码重用。

  • 类型:

    • 实现继承:子类直接使用基类的属性和方法。
    • 接口继承:子类实现基类的纯虚函数。
    • 可视继承:子类使用基类的外观和实现代码。
  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Animal {
    public:
    void eat() {
    std::cout << "Animal is eating." << std::endl;
    }
    };

    class Dog : public Animal {
    public:
    void bark() {
    std::cout << "Dog is barking." << std::endl;
    }
    };

    int main() {
    Dog dog;
    dog.eat(); // 从 Animal 继承
    dog.bark();
    return 0;
    }

3. 多态(Polymorphism)

  • 定义:同一个接口可以有不同的实现,向不同对象发送同一消息,不同对象产生不同的行为。

  • 实现方式:

    • 覆盖(override):子类重新定义父类的虚函数。
    • 重载(overload):同名函数参数不同。
  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    class Animal {
    public:
    virtual void makeSound() {
    std::cout << "Animal sound." << std::endl;
    }
    };

    class Dog : public Animal {
    public:
    void makeSound() override {
    std::cout << "Dog barks." << std::endl;
    }
    };

    class Cat : public Animal {
    public:
    void makeSound() override {
    std::cout << "Cat meows." << std::endl;
    }
    };

    int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->makeSound(); // Dog barks.
    animal2->makeSound(); // Cat meows.

    delete animal1;
    delete animal2;

    return 0;
    }

二、访问修饰符

C++通过publicprotectedprivate 三个关键字来控制成员变量和成员函数的访问权限。

  • public:公有成员可以被类外部的任何代码访问。
  • protected:保护成员只能被类的内部和派生类访问。
  • private:私有成员只能被类的内部访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Base {
public:
int publicVar;
protected:
int protectedVar;
private:
int privateVar;
};

class Derived : public Base {
public:
void accessMembers() {
publicVar = 1; // 可以访问
protectedVar = 2; // 可以访问
// privateVar = 3; // 无法访问
}
};

int main() {
Base base;
base.publicVar = 1; // 可以访问
// base.protectedVar = 2; // 无法访问
// base.privateVar = 3; // 无法访问
return 0;
}

三、什么是多重继承?

C++ 支持一个类从多个基类继承属性和行为,这称为多重继承。多重继承可能引入一些问题,如菱形继承问题,为了解决这些问题,C++ 提供了虚继承, 通过在继承声明中使用 virtual关键字,可以避免在派生类中生成多个基类的实例,从而解决了菱形继承带来的二义性。

菱形继承问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>

class Animal {
public:
void eat() {
std::cout << "Animal is eating." << std::endl;
}
};

class Mammal : public Animal {
public:
void breathe() {
std::cout << "Mammal is breathing." << std::endl;
}
};

class Bird : public Animal {
public:
void fly() {
std::cout << "Bird is flying." << std::endl;
}
};
// 菱形继承,同时从 Mammal 和 Bird 继承
class Bat : public Mammal, public Bird {
public:
void navigate() {
// 这里可能会引起二义性,因为 Bat 继承了两个 Animal
eat(); // 不明确应该调用哪个基类的 eat
}
};

int main() {
Bat bat;
bat.navigate(); // 可能引起错误
return 0;
}

image-20240622100752448

解决菱形继承问题的虚继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

class Animal {
public:
void eat() {
std::cout << "Animal is eating." << std::endl;
}
};

class Mammal : virtual public Animal {
public:
void breathe() {
std::cout << "Mammal is breathing." << std::endl;
}
};

class Bird : virtual public Animal {
public:
void fly() {
std::cout << "Bird is flying." << std::endl;
}
};

class Bat : public Mammal, public Bird {
public:
void navigate() {
// 不再存在二义性,eat 方法来自于共享的 Animal 基类
eat();
}
};

int main() {
Bat bat;
bat.navigate();
return 0;
}

四、重载与重写

1. 重载(Overloading)

重载是指在同一个作用域内,定义多个具有相同名称但参数列表或类型不同的函数。重载允许使用相同的函数名处理不同类型或数量的参数,提供多个版本的功能。

示例:函数重载

1
2
3
4
5
6
7
8
9
10
11
int add(int a, int b) {
return a + b;
}

double add(double a, double b) {
return a + b;
}

std::string add(const std::string& a, const std::string& b) {
return a + b;
}

在上述示例中,函数add被重载了三次,分别处理整数、浮点数和字符串的加法运算。

重载规则:

  1. 参数类型或数量必须不同。
  2. 不能仅通过返回类型来区分重载的函数。
  3. 函数的访问权限和抛出的异常不会影响重载。

2. 重写(Overriding)

重写是指派生类重新定义基类中的虚函数,以提供特定于派生类的实现。重写是面向对象编程中的多态性的一种体现,允许通过基类指针或引用调用派生类的实现。

示例:函数重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;

class Base {
public:
virtual void print() {
cout << "Base class" << endl;
}
};

class Derived : public Base {
public:
void print() override {
cout << "Derived class" << endl;
}
};

在上述示例中,Derived类重写了基类Base中的虚函数print

重写规则:

  1. 重写的方法必须有相同的参数列表和返回类型。
  2. 被重写的方法不能为private
  3. 静态方法不能被重写为非静态方法。
  4. 重写方法的访问修饰符必须大于或等于被重写方法的访问修饰符。

重载与重写的区别

  1. 作用范围不同
    • 重载发生在同一个类中。
    • 重写发生在基类和派生类之间。
  2. 函数签名
    • 重载要求函数名相同,但参数列表必须不同。
    • 重写要求函数名、参数列表和返回类型都相同。
  3. 实现目的
    • 重载用于实现不同参数版本的同一个功能。
    • 重写用于在派生类中提供基类虚函数的具体实现。

五、多态

C++中的多态性通过虚函数(virtual function)和虚函数表(vtable)来实现。编译器在对象的内存布局中维护了一个虚函数表,其中存储了指向实际函数的指针。这个表在运行时用于动态查找调用的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

class Shape {
public:
virtual void draw() const {
cout << "Drawing Shape" << endl;
}
};

class Circle : public Shape {
public:
void draw() const override {
cout << "Drawing Circle" << endl;
}
};

void display(Shape* shape) {
shape->draw();
}

int main() {
Shape* shape = new Circle();
display(shape); // 调用的是Circle类的draw方法
delete shape;
return 0;
}

六、成员函数变量/静态成员函数/变量的区别

  1. 成员函数
  • 属于类的函数,可以访问类的成员变量和其他成员函数。
  • 通过对象调用,可以访问对象的成员变量。
1
2
3
4
5
6
class MyClass {
public:
void memberFunction() {
// 成员函数的实现
}
};
  1. 成员变量
  • 属于类的变量,每个对象都有一份成员变量的副本。
1
2
3
4
class MyClass {
public:
int memberVariable; // 成员变量的声明
};
  1. 静态成员函数
  • 属于类而不是对象,可以直接通过类名调用,不需要创建类的实例。
  • 不能直接访问普通成员变量,因为没有隐含的this指针。
1
2
3
4
5
6
class MyClass {
public:
static void staticMemberFunction() {
// 静态成员函数的实现
}
};
  1. 静态成员变量
  • 属于类而不是对象,在所有对象之间共享。
  • 在类的定义外进行定义和初始化。
1
2
3
4
5
6
class MyClass {
public:
static int staticMemberVariable; // 静态成员变量的声明
};

int MyClass::staticMemberVariable = 0; // 静态成员变量的定义和初始化

七、构造函数和析构函数

  1. 构造函数

    • 在创建对象时自动调用,用于初始化对象的成员变量和分配资源。
    • 函数名与类名相同,没有返回类型。
    • 可以有多个构造函数(重载)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class MyClass {
    public:
    MyClass() {
    // 默认构造函数
    }

    MyClass(int value) {
    // 带参数的构造函数
    }
    };
  2. 析构函数

    • 在对象生命周期结束时自动调用,用于释放对象占用的资源。
    • 函数名与类名相同,前面加上波浪号~,没有参数,也不能重载。
    1
    2
    3
    4
    5
    6
    class MyClass {
    public:
    ~MyClass() {
    // 析构函数
    }
    };

C++构造函数的种类

  1. 默认构造函数:没有参数的构造函数。

    1
    2
    3
    4
    5
    6
    class MyClass {
    public:
    MyClass() {
    // 默认构造函数
    }
    };
  2. 带参数的构造函数:接受一个或多个参数,用于在创建对象时传递初始化值。

    1
    2
    3
    4
    5
    6
    class MyClass {
    public:
    MyClass(int value) {
    // 带参数的构造函数
    }
    };
  3. 拷贝构造函数:用于通过已存在的对象创建一个新对象,参数通常是对同类型对象的引用。

    1
    2
    3
    4
    5
    6
    class MyClass {
    public:
    MyClass(const MyClass &other) {
    // 拷贝构造函数
    }
    };
  4. 委托构造函数:在一个构造函数中调用同类的另一个构造函数,减少代码重复。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class MyClass {
    public:
    MyClass() : MyClass(0) {
    // 委托构造函数
    }

    MyClass(int value) {
    // 带参数的构造函数
    }
    };

通过理解和应用这些概念,可以更好地掌握C++面向对象编程的核心思想,编写出结构清晰、功能强大的代码。

八、虚函数和虚函数表

1. 虚函数(Virtual Function)

虚函数是C++中的一种机制,允许在基类中声明一个函数,并在派生类中重写该函数,从而实现多态性。这使得通过基类指针或引用调用派生类的函数成为可能,而不需要知道派生类的具体类型。

虚函数的定义和使用

在基类中使用 virtual 关键字来声明虚函数。派生类中可以选择重写这个虚函数,重写时不需要再使用 virtual 关键字,但可以加上以增加代码可读性。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>

// 基类
class Base {
public:
virtual void show() {
std::cout << "Base class show function" << std::endl;
}

virtual ~Base() { // 虚析构函数
std::cout << "Base class destructor" << std::endl;
}
};

// 派生类
class Derived : public Base {
public:
void show() override { // 重写虚函数
std::cout << "Derived class show function" << std::endl;
}

~Derived() {
std::cout << "Derived class destructor" << std::endl;
}
};

void demonstrateVirtualFunction() {
Base* b = new Derived();
b->show(); // 调用的是派生类的 show 函数
delete b; // 调用的是派生类的析构函数,然后是基类的析构函数
}

int main() {
demonstrateVirtualFunction();
return 0;
}

解释

  • 虚函数的调用:在示例中,通过基类指针 b 调用 show 函数时,调用的是派生类 Derived 中重写的 show 函数。这是通过虚函数表(vtable)实现的,编译器会在运行时决定调用哪个版本的函数。
  • 虚析构函数:基类的析构函数被声明为虚函数,以确保在通过基类指针删除对象时,能够调用派生类的析构函数,避免资源泄漏。

2. 虚函数的实现

虚函数在C++中是通过一种被称为虚函数表(Virtual Table, vtable)和虚指针(Virtual Pointer, vptr)机制实现的。这是一个编译器实现的机制,用于支持运行时的多态性。

虚函数表(vtable)

每个定义了虚函数的类(或其派生类)都有一个虚函数表。虚函数表是一个指针数组,其中每个元素都是指向该类的虚函数的指针。虚函数表在编译时生成,并在运行时使用。

虚指针(vptr)

每个包含虚函数的类的对象都包含一个隐藏的指针,称为虚指针(vptr)。这个指针指向该类的虚函数表。当对象创建时,vptr会被设置为指向相应类的虚函数表。

虚函数调用过程

  1. 对象创建

    • 当一个对象被创建时,编译器会设置该对象的vptr,指向对应类的vtable。
  2. 虚函数调用

    • 当通过基类指针或引用调用虚函数时,程序会通过对象的vptr找到该对象的vtable。
    • 然后,通过查找vtable中的相应函数指针,调用实际的函数实现。

示例代码

以下是一个简化的示例代码,展示了虚函数表和虚指针的工作原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

// 基类
class Base {
public:
virtual void func1() { std::cout << "Base::func1" << std::endl; }
virtual void func2() { std::cout << "Base::func2" << std::endl; }
virtual ~Base() {}
};

// 派生类
class Derived : public Base {
public:
void func1() override { std::cout << "Derived::func1" << std::endl; }
void func2() override { std::cout << "Derived::func2" << std::endl; }
};

int main() {
Base* basePtr = new Derived();
basePtr->func1(); // 输出 Derived::func1
basePtr->func2(); // 输出 Derived::func2
delete basePtr;
return 0;
}

详细解释

  1. 类的虚函数表

    • Base类的vtable包含指向Base::func1Base::func2的指针。
    • Derived类的vtable包含指向Derived::func1Derived::func2的指针。
  2. 对象创建时的vptr设置

    • Derived类对象创建时,其vptr被设置为指向Derived类的vtable。
  3. 虚函数调用过程

    • 当调用basePtr->func1()时,程序通过basePtr的vptr找到Derived类的vtable,然后调用Derived::func1
    • 同理,调用basePtr->func2()时,调用Derived::func2

虚函数表和虚指针的图示

以下是一个简化的图示:

1
2
3
4
5
6
7
8
9
10
Base vtable:
[0] &Base::func1
[1] &Base::func2

Derived vtable:
[0] &Derived::func1
[1] &Derived::func2

Derived object:
vptr -> Derived vtable

通过这种方式,C++实现了运行时的多态性,使得基类指针或引用可以调用派生类的函数。这种机制虽然增加了一些开销,但带来了极大的灵活性和可扩展性。

3. 虚函数表(vtable)

虚函数表是由编译器维护的一种机制,用于支持运行时的多态性。每个包含虚函数的类都有一个虚函数表,其中存储了该类的虚函数的指针。

  • 每个对象包含一个指向其类的虚函数表的指针
  • 当通过基类指针调用虚函数时,程序会查找该指针所指向的虚函数表,并调用对应的函数

4. 纯虚函数和抽象类

当一个类中包含一个或多个纯虚函数时,该类被称为抽象类,无法实例化。纯虚函数的定义方式如下:

1
2
3
4
class AbstractClass {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数
};

任何继承自 AbstractClass 的类都必须实现 pureVirtualFunction 函数,否则也将成为抽象类。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

// 抽象类
class AbstractClass {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数
};

// 派生类
class ConcreteClass : public AbstractClass {
public:
void pureVirtualFunction() override {
std::cout << "Implementation of pure virtual function" << std::endl;
}
};

void demonstratePureVirtualFunction() {
ConcreteClass obj;
obj.pureVirtualFunction(); // 调用实现的纯虚函数
}

int main() {
demonstratePureVirtualFunction();
return 0;
}

5. 虚函数和纯虚函数的区别

  1. 虚函数

    • 有实现:虚函数在基类中有函数声明和实现。
    • 可选实现:派生类可以选择是否覆盖虚函数。
    • 允许实例化:包含虚函数的类可以被实例化。
    • 调用方式:根据对象的实际类型来决定调用哪个版本的虚函数。
    • 声明:使用 virtual 关键字声明,但不包含 = 0
    1
    2
    3
    4
    5
    6
    class Base {
    public:
    virtual void virtualFunction() {
    // 具体实现
    }
    };
  2. 纯虚函数

    • 没有实现:纯虚函数没有函数体,只有函数声明。
    • 强制覆盖:派生类必须提供纯虚函数的具体实现,否则它们也会成为抽象类。
    • 禁止实例化:包含纯虚函数的类无法被实例化,只能用于派生其他类。
    • 声明:使用 = 0 在函数声明末尾进行声明。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class AbstractBase {
    public:
    // 纯虚函数,没有具体实现
    virtual void pureVirtualFunction() = 0;

    // 普通成员函数可以有具体实现
    void commonFunction() {
    // 具体实现
    }
    };

6. 虚析构函数

虚析构函数是带有 virtual 关键字的析构函数。它的主要作用是确保在通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,从而释放对象所占用的资源。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
virtual ~Base() {
// 基类析构函数的实现
}
};

class Derived : public Base {
public:
~Derived() override {
// 派生类析构函数的实现
}
};

(1) 为什么需要虚析构函数?

确保正确的资源释放

虚析构函数允许在运行时根据对象的实际类型调用正确的析构函数,从而实现多态性。如果基类的析构函数不是虚的,当通过基类指针删除指向派生类对象的对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类的资源未被正确释放,造成内存泄漏。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
};

class Derived : public Base {
public:
~Derived() override {
std::cout << "Derived destructor" << std::endl;
}
};

void example() {
Base* basePtr = new Derived();
delete basePtr; // 调用 Derived 和 Base 的析构函数
}

int main() {
example();
return 0;
}

在这个例子中,通过基类指针删除派生类对象时,正确地调用了派生类和基类的析构函数,从而确保了资源的正确释放。

(2) 为什么构造函数不能是虚函数?

  • 对象类型的确定

构造函数在对象的创建阶段被调用,对象的类型在构造函数中已经确定。因此,构造函数调用不涉及多态性,也就是说,在对象的构造期间无法实现动态绑定。

  • 存储空间角度

虚函数的机制依赖于虚函数表(vtable),这个表的地址存储在对象的内存空间中。如果将构造函数设置为虚函数,那么在对象还没有实例化且内存空间还没有分配时,就需要访问虚函数表,这是不可能实现的。

  • 使用角度

虚函数主要用于在信息不全的情况下,使得重载的函数能够根据对象的实际类型进行调用。而构造函数的作用是初始化实例,在对象的创建时自动调用,不需要通过父类的指针或引用来调用。因此,构造函数没有必要是虚函数。

  • 实现角度

虚函数表是在构造函数调用后才建立的,因此构造函数不可能成为虚函数。从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调用父类的构造函数)。而且构造函数的作用是提供初始化,在对象生命周期中只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数。

示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Base {
public:
// 错误!不能声明虚构造函数
// virtual Base() { }

virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
};

class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor" << std::endl;
}

~Derived() override {
std::cout << "Derived destructor" << std::endl;
}
};

void example() {
Base* basePtr = new Derived();
delete basePtr; // 调用 Derived 和 Base 的析构函数
}

int main() {
example();
return 0;
}

在这个示例中,通过基类指针删除派生类对象时,确保了派生类和基类的析构函数都被正确调用,从而释放了所有资源。而构造函数不能是虚函数,因为它们的调用不涉及多态性和动态绑定。

7. 不能声明为虚函数的函数

(1) 构造函数

原因:构造函数在对象的创建阶段调用,对象的类型在构造期间已经确定。虚函数的动态绑定在运行时实现,而构造函数在对象还未创建完全时就会被调用。

  • 构造函数的主要目的是初始化对象,这在对象的类型已经确定时进行,而不需要多态性。
  • 虚函数表(vtable)是在构造函数调用之后建立的,因此在构造函数中无法进行虚函数的动态绑定。

(2) 普通函数(非成员函数)

原因:普通函数(非成员函数)不能参与类的继承,因此不能被声明为虚函数。

  • 非成员函数没有 this 指针,不能参与类的继承和多态机制。
  • 虚函数主要用于实现类的多态性,而非成员函数不属于任何类。

(3) 静态成员函数

原因:静态成员函数不与任何对象关联,不能访问非静态成员,因此不能参与类的多态性。

  • 静态成员函数对于每个类来说只有一份代码,所有对象共享这一份代码。
  • 静态成员函数没有 this 指针,无法访问对象的动态类型,因此无法实现虚函数的动态绑定。

(4) 友元函数

原因:友元函数不属于类的成员,不能继承,因此不能声明为虚函数。

  • 友元函数是为了访问类的私有成员而定义的,但它们本身不属于类。
  • 友元函数不参与类的继承和多态性,因此不能声明为虚函数。

(5) 内联成员函数

原因:内联函数在编译时展开,虚函数在运行时动态绑定,两者的机制不兼容。

  • 内联函数的目的是在编译时展开代码,减少函数调用的开销。
  • 虚函数的动态绑定是在运行时实现的,内联展开与动态绑定的时机不同,因此无法将内联函数声明为虚函数。

九、深拷⻉和浅拷⻉的区别

1. 深拷贝(Deep Copy)

定义:深拷贝是对对象的完全独立复制,包括对象内部动态分配的资源。

特点

  • 复制对象及其所有成员变量的
  • 动态分配的资源也会被复制,新对象拥有自己的一份资源副本。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>  

class DeepCopyExample {
public:
DeepCopyExample(int value) : data(new int(value)) {}
DeepCopyExample(const DeepCopyExample& other) // 自定义复制构造函数(深拷贝)
: data(new int(*other.data)) {}

DeepCopyExample& operator=(const DeepCopyExample& other) // 自定义赋值运算符重载(深拷贝)
{
if (this != &other) {
delete data; // 释放原有内存
data = new int(*other.data); // 分配新内存并复制数据
}
return *this;
}

~DeepCopyExample() { delete data; }

int getValue() const { return *data; }

private:
int* data;
};

int main() {
DeepCopyExample obj1(10);
DeepCopyExample obj2 = obj1; // 使用自定义的复制构造函数(深拷贝)

// 修改 obj2 的数据
*obj2.data = 20;

// obj1 的数据没有被修改,因为它们是深拷贝
std::cout << "obj1.getValue(): " << obj1.getValue() << std::endl; // 输出:obj1.getValue(): 10
std::cout << "obj2.getValue(): " << obj2.getValue() << std::endl; // 输出:obj2.getValue(): 20

return 0;
}

2. 浅拷贝(Shallow Copy)

定义:浅拷贝仅复制对象的值,而不涉及对象内部动态分配的资源。

特点

  • 复制对象及其所有成员变量的值。
  • 对象内部动态分配的资源不会被复制,新对象和原对象共享同一份资源

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>  

class ShallowCopyExample {
public:
ShallowCopyExample(int value) : data(new int(value)) {}
~ShallowCopyExample() { delete data; }

// 注意:这里没有提供自定义的复制构造函数或赋值运算符重载

int getValue() const { return *data; }

private:
int* data;
};

int main() {
ShallowCopyExample obj1(10);
ShallowCopyExample obj2 = obj1; // 使用默认的复制构造函数(浅拷贝)

// 修改 obj2 的数据
*obj2.data = 20;

// obj1 和 obj2 的 data 指针指向了同一块动态分配的内存。
// 当 obj2 的数据被修改时,obj1 的数据也被修改了,因为它们是浅拷贝。
std::cout << "obj1.getValue(): " << obj1.getValue() << std::endl; // 输出:obj1.getValue(): 20
std::cout << "obj2.getValue(): " << obj2.getValue() << std::endl; // 输出:obj2.getValue(): 20

return 0;
}

十、运算符重载(Operator Overloading)

运算符重载允许我们为自定义的类定义新的运算符行为,使得这些类可以像内置类型一样使用运算符。本质上,运算符重载是对函数调用的另一种语法形式。

1. 基本语法

运算符重载函数可以作为成员函数或非成员函数定义。以下是运算符重载的基本形式:

  1. 成员函数重载

    • 适用于需要访问类的私有成员。
    • 适用于左操作数为类对象的情况。
  2. 非成员函数重载

    • 适用于对称性操作符,如算术和关系运算符。
    • 通常定义为友元函数,以访问类的私有成员。

示例代码

成员函数重载示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Complex {
private:
double real, imag;

public:
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}

// 重载加法运算符
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}

// 重载输出运算符
friend std::ostream& operator<<(std::ostream& out, const Complex& c) {
out << "(" << c.real << ", " << c.imag << ")";
return out;
}
};

int main() {
Complex c1(1.0, 2.0), c2(2.0, 3.0);
Complex c3 = c1 + c2;
std::cout << c3 << std::endl;
return 0;
}

2. 运算符重载规则

  1. 算术和关系运算符

    • 通常定义为非成员函数,以保证对称性。
    • 参数为常量引用。
  2. 赋值运算符

    • 必须是成员函数。
    • 通常返回 *this 以实现链式赋值。
  3. 复合赋值运算符

    • 建议定义为成员函数。
    • 例如 +=, -=, *=, /=, %= 等。
  4. 下标运算符

    • 必须是成员函数。
    • 返回元素的引用,建议提供常量和非常量版本。
  5. 递增递减运算符

    • 建议定义为成员函数。
    • 区分前置和后置,前置返回自增/自减后的对象引用,后置返回对象的原值(非引用)。
  6. 解引用运算符

    • 建议定义为成员函数,因为它与给定类型关系密切。
  7. 箭头运算符

    • 必须是成员函数。

例外情况

某些运算符不建议重载或需要特别小心:

  1. 逗号运算符

    • 已经对类类型有特殊定义,不建议重载。
  2. 取地址运算符

    • 通常不需要重载,因为其默认行为已符合大多数情况。
  3. 逻辑与、逻辑或运算符

    • 有短路求值属性,重载可能导致不符合预期的行为。

3. 函数调用运算符

函数调用运算符 () 可以重载,用于创建类似函数对象(functor)或自定义行为。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Adder {
private:
int increment;

public:
Adder(int inc) : increment(inc) {}

int operator()(int x) const {
return x + increment;
}
};

int main() {
Adder addFive(5);
std::cout << addFive(10) << std::endl; // 输出 15
return 0;
}

结构体(struct)和类(class)在C++中是两种用于定义自定义数据类型的关键字,它们有一些重要的区别和共同点:

十一、结构体和类的区别

1. 成员访问权限

  • 结构体默认的成员访问权限为公共(public)。

  • 默认的成员访问权限为私有(private)。

2. 成员函数

  • 结构体中可以包含成员函数,但不常见,通常用于简单数据结构。

  • 中常常包含成员函数,用于操作和控制类的数据成员,支持面向对象的编程。

3. 继承

  • 结构体可以继承其他结构体或类,默认继承方式是公共继承。

  • 可以继承其他类或结构体,支持多种继承方式(公共继承、保护继承、私有继承)。

4. 默认访问控制

  • 结构体成员默认为公共的,可以被外部访问。

  • 成员默认为私有的,只能在类内部或友元函数中访问。

5. 对象初始化

  • 结构体可以使用默认成员初始化列表 {} 进行初始化。

  • 可以定义构造函数、析构函数等,更灵活地控制对象的初始化和销毁过程。

示例比较

结构体示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Base {
int a;
};

struct Derived : Base {
int b;
};

int main() {
Derived d;
d.a = 10; // 合法,默认public继承
d.b = 20;
return 0;
}

类示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
int a;
public:
Base() : a(0) {}
int getA() { return a; }
};

class Derived : public Base {
int b;
public:
Derived() : b(0) {}
int getB() { return b; }
};

int main() {
Derived d;
// d.a = 10; // 错误,a是private
d.getA(); // 合法,通过public方法访问
return 0;
}

总结

  • 结构体适合简单的数据集合,成员默认公共。

  • 适合复杂的对象模型,支持封装、继承和多态,成员默认私有。

根据需求和设计理念选择合适的关键字来定义自定义数据类型,在实际应用中,结构体和类都有其独特的优势和适用场景。

评论