类和对象
1 |
|
创建对象
栈上创建 (dog1
)
解释:
- 声明和定义:
Dog dog1;
在栈上声明并定义了一个Dog
类型的对象dog1
。栈上的对象在定义它的作用域结束时自动销毁。 - 成员赋值: 通过点操作符
.
,您可以访问和修改dog1
对象的公共成员变量name
和age
。 - 调用成员函数:
dog1.run();
调用了dog1
对象的run
成员函数,输出小狗的名字和年龄。 - 生命周期: 当
main
函数执行完毕,dog1
对象的生命周期结束,它的析构函数(如果有的话)将被调用,释放它所占用的资源。
堆上创建 (dog2
)
解释:
- 动态分配:
Dog *dog2 = new Dog();
使用new
关键字在堆上动态分配了一个Dog
类型的对象,并返回了这个对象的指针。指针dog2
存储了对象在内存中的地址。 - 成员赋值: 由于
dog2
是一个指针,您需要使用箭头操作符->
来访问对象的成员变量。 - 调用成员函数: 同样使用箭头操作符
->
来调用dog2
指向的对象的成员函数。 - 手动管理生命周期: 在堆上分配的对象不会自动销毁,需要程序员手动管理。使用
delete dog2;
来释放dog2
指向的内存,防止内存泄漏。之后,将dog2
设置为NULL
是一个好习惯,这样可以避免悬空指针的问题,即避免使用已经释放的内存。
总结
- 栈上创建的对象生命周期由编译器管理,简单且安全,但栈空间有限,不适合创建大量或大对象。
- 堆上创建的对象生命周期由程序员管理,提供了更大的灵活性,但需要手动释放内存,容易出错(如忘记释放或重复释放内存)。
两种方式的选择取决于具体的应用场景和性能要求。在大多数情况下,简单的对象可以使用栈分配,而复杂或大量对象则适合使用堆分配。
构造函数与析构函数
属于特殊的成员函数
构造函数
构造函数在对象实例化时被系统自动调用,仅调用一次。定义类时,如果没有定义构造函数和析构函数,编译器就会生成一个构造函数和析构函数,只是这个构造函数和析构函数什么事情也不做。
构造函数的特点如下:
- 构造函数必须与类名同名
- 可以重载
- 没有返回类型,即使是void也不行
析构函数
与构造函数相反,在对象结束其生命周期时系统自动执行析构函数。实际上定义类时,编译器会生成一个析构函数
析构函数的特点如下:
- 析构函数的格式为~类名()
- 调用时释放内存(资源)
- ~类名()不能加参数
- 没有返回值,即使是void也不行
1 |
|
执行结果:
构造函数执行!
构造函数与析构函数示例
析构函数执行!
对象的生命周期
在 C++ 中,对象的生命周期指的是对象从创建到销毁的整个过程。生命周期结束意味着对象不再存在,其占用的内存将被回收,并且与该对象相关的资源(如打开的文件句柄、网络连接等)也将被释放。以下是详细说明:
对象生命周期的几个阶段:
- 创建(Construction):
- 当对象被创建时,其生命周期开始。这可以通过多种方式完成,例如在栈上声明、在堆上使用
new
分配或者在全局/静态存储区中声明。
- 当对象被创建时,其生命周期开始。这可以通过多种方式完成,例如在栈上声明、在堆上使用
- 存在(Existence):
- 在这个阶段,对象是有效的,可以访问其成员变量和调用成员函数。
- 销毁(Destruction):
- 对象的生命周期结束,通常由以下情况触发:
- 栈上的局部对象:当包含它们的代码块(如函数体)结束时。
- 堆上的对象:当使用
delete
操作符显式释放它们时。 - 全局/静态对象:当程序结束时。
- 对象的生命周期结束,通常由以下情况触发:
生命周期结束的具体含义:
- 栈对象:
- 对于在栈上创建的对象,当它们离开作用域(例如,函数返回)时,它们的析构函数(如果有的话)会被自动调用,随后对象占用的内存将被回收。
- 堆对象:
- 对于在堆上创建的对象,必须使用
delete
操作符来显式结束其生命周期。当delete
被调用时,对象的析构函数会被执行,然后对象占用的内存被回收。
- 对于在堆上创建的对象,必须使用
- 全局/静态对象:
- 全局或静态对象的析构函数会在程序结束时自动调用。
生命周期结束后的行为:
- 成员变量:
- 对象的成员变量所占用的内存也随之释放。
- 资源管理:
- 如果对象负责管理资源(如动态分配的内存、文件句柄等),则其析构函数通常会负责释放这些资源。
- 引用或指针:
- 如果有其他变量(如引用或指针)指向该对象,那么这些变量将变成悬空引用或悬空指针,继续使用它们将导致未定义行为。
理解对象的生命周期对于编写高效且无内存泄漏的 C++ 程序至关重要。正确管理对象的生命周期可以帮助避免许多常见的编程错误,如内存泄漏、悬挂指针和资源泄露。
- 如果有其他变量(如引用或指针)指向该对象,那么这些变量将变成悬空引用或悬空指针,继续使用它们将导致未定义行为。
this指针
一个类的不同对象在调用自己的成员函数时,其实他们调用的是同一段函数代码,那么成员函数如何知道要访问哪个对象的数据成员呢?
没错,就是通过this指针。每个对象都有一个this指针,this指针记录对象的内存地址。在C++中,this指针是指向类自身数据的指针,简单的来说就是指向当前类的当前实例对象。
关于类的this指针有以下特点:
- this只能在成员函数中使用,全局函数、静态函数都不能使用this。实际上,成员函数默认第一个参数为T *const this。也就是一个类里面的成员函数int func(int p),func的原型在编译器看来应该是int func(T *const this,int p)
- this在成员函数的开始前构造,在成员函数的结束后清除
- this指针会因编译器不同而有不同的放置位置。可能是栈,也可能是寄存器,甚至全局变量
1 |
|
在 C++ 中,Person(const string& name) : name(name) {}
是一个类 Person
的构造函数的定义,使用了初始化列表来初始化成员变量。
让我们分解这个构造函数:
Person(const string& name)
:这是构造函数的声明,它接受一个const string&
类型的参数name
。使用const string&
是一种引用传递的方式,这样可以避免不必要的字符串拷贝,并且保证传递的字符串在函数内部不会被修改。: name(name)
:这是构造函数的初始化列表部分。在这里,name(name)
表示将构造函数的参数name
的值传递给类的成员变量name
。在初始化列表中,name
出现了两次,第一个name
是成员变量的名字,第二个name
是构造函数参数的名字。这种写法有时会导致一些混淆,但它是在告诉编译器,将传入的name
参数的值赋给成员变量name
。{}
:这是构造函数的函数体。在这个例子中,函数体是空的,因为所有的初始化工作已经在初始化列表中完成了。
综上所述,这个构造函数的作用是创建一个Person
对象,并将其name
成员变量初始化为传入的参数值。使用初始化列表是一种效率较高的初始化成员变量的方式,特别是当成员变量是引用或常量时,它必须在构造函数体执行之前被初始化。
继承
面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。在QT里大量的使用了这种特性,当QT里的类不满足自己的要求是,我们可以重写这个类,就是通过继承需要重写的类,来实现自己的类的功能。
一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:
1 | class derived-class: access-specifier base-class |
与类的访问修饰限定符一样,继承的方式也有几种。其中,访问修饰符access-specifier
是public、protect或private其中的一个,base-class是之前定义过的某个类的名称。如果未使用访问修饰符access-specifier
,则默认为private。
公有继承public
当一个类派生继承公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问
保护继承protected
当一个类派生继承保护基类时,基类的公有和保护成员将成为派生类的保护成员
私有继承private
当一个类派生继承私有基类时,基类的公有和保护成员将成为派生类的私有成员
在面向对象的编程语言中,如 C++,类成员可以被指定为公有(public)、保护(protected)或私有(private),这些关键字用于实现封装,即控制类成员的访问权限。以下是这三种成员的详细解释:
公有成员(Public Members)
公有成员是在类定义中使用 public
关键字声明的成员。公有成员可以被类的对象直接访问,也可以被类的外部函数访问。
- 特点:
- 可以被类的任何对象访问。
- 可以在类的外部通过对象直接访问。
- 通常包含类的接口,即那些用于与类交互的函数和属性。
- 示例:在这个例子中,
1
2
3
4
5
6
7
8
9
10
11class Person {
public:
void setName(const std::string& newName) {
name = newName;
}
std::string getName() const {
return name;
}
private:
std::string name;
};setName
和getName
是公有成员函数,它们可以被任何Person
类的对象调用。
保护成员(Protected Members)
保护成员是在类定义中使用protected
关键字声明的成员。保护成员的行为类似于私有成员,但它们可以被派生类访问。 - 特点:
- 不能被类的对象直接访问。
- 可以被类的成员函数访问。
- 可以被派生类(子类)的成员函数访问。
- 示例:在这个例子中,
1
2
3
4
5
6
7
8
9
10class Base {
protected:
int protectedData;
};
class Derived : public Base {
public:
void setProtectedData(int data) {
protectedData = data; // 在派生类中可以访问
}
};protectedData
是保护成员,它不能被Base
类的对象直接访问,但可以在Derived
类的成员函数setProtectedData
中访问。
私有成员(Private Members)
私有成员是在类定义中使用private
关键字声明的成员。私有成员只能被类的成员函数访问,不能被类的对象或类的外部函数直接访问。 - 特点:
- 不能被类的对象直接访问。
- 可以被类的成员函数访问。
- 不能被派生类访问,除非通过基类的公有或保护成员函数。
- 示例:在这个例子中,
1
2
3
4
5
6
7
8
9
10
11
12
13class Person {
public:
void setAge(int newAge) {
if (newAge >= 0) {
age = newAge;
}
}
int getAge() const {
return age;
}
private:
int age;
};age
是私有成员,它不能被Person
类的对象直接访问,但可以通过公有成员函数setAge
和getAge
来设置和获取。
通过使用公有、保护和私有成员,C++ 类可以隐藏其内部实现细节,只暴露必要的接口,这是面向对象编程中的一个重要原则。
1 |
|
重载
C++允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载
和运算符重载
重载声明是指一个与之前已经在该作用域声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同
当调用时,编译器通过使用的参数类型和定义中的参数类型进行比较。决定选用最合适的定义,这个过程称为重载决策
函数重载
同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数必须不同。我们不能仅通过返回类型的不同来重载函数。
1 |
|
避免用户传入的参数类型不在我们写的重载函数里,还可以多写几个类型的
运算符重载
运算符重载的实质就是函数重载或函数多态。运算符重载是一种形式的C++多态。要重载运算符,需要使用被称为运算符函数的特殊函数形式,运算符函数形式:operatorp(argument-list),operator后面的p
为要重载的运算符符号。
1 | <返回类型说明符>operator<运算符符号>(<参数表>) |
多态
C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数;
形成多态必须具备三个条件:
- 必须存在继承关系
- 继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字virtual声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数)
- 存在基类类型的指针或引用,通过该指针或引用调用虚函数
虚函数
是在基类中使用关键字virtual声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。虚函数声明如下:
1 | virtual ReturnType FunctionName(Parameter) |
虚函数必须实现。如果不实现,编译器将报错
纯虚函数
若在基类中定义虚函数,以便在派生类中重新定义改函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。纯虚函数声明如下:
1 | virtual void funtion1()=0; |
纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向该抽象类的具体类的指针或引用。
1 |
|
数据封装
封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受外界的干扰和误用,从而保证了安全。数据封装引申出了另一个重要的OOP概念,即数据隐藏。
数据封装是一种把数据和操作数据的函数绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制,C++通过创建类来支持封装和数据隐藏(public、protected、private)。
1 |
|