在编程的世界里,C++以其强大的灵活性和高效性,在众多编程语言中占据了举足轻重的地位。它不仅继承了C语言的底层操作能力和高效执行速度,还引入了面向对象编程(OOP)的概念,极大地提升了代码的可维护性、可扩展性和重用性。其中,类和对象是C++面向对象编程的核心,它们为程序员提供了一种组织代码、模拟现实世界实体以及实现复杂数据结构的有效方式。
面向过程编程(Procedural Programming)是一种以过程(或函数)为中心的编程范式。在这种编程方式中,程序主要由一系列指令和函数构成,程序员通过函数调用来组织代码,函数的核心目的是处理数据。
#include <iostream>
// 函数定义
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(5, 10);
std::cout << "结果是: " << result << std::endl;
return 0;
}
面向对象编程(Object-Oriented Programming, OOP)是C++的核心编程范式之一。它强调使用“对象”来组织代码,程序中的数据和操作被封装在对象中,具有更好的模块化和重用性。
#include <iostream>
// 定义一个类
class Rectangle {
private:
int width, height;
public:
// 构造函数
Rectangle(int w, int h) {
width = w;
height = h;
}
// 成员函数
int area() {
return width * height;
}
};
int main() {
// 创建对象
Rectangle rect(10, 20);
// 调用成员函数
std::cout << "矩形的面积是: " << rect.area() << std::endl;
return 0;
}
C++作为一门强大的编程语言,能够灵活地支持这两种编程方式,帮助开发者根据不同的需求选择合适的编程范式。
类(Class)可以被看作是一个蓝图或模板,它定义了某种对象的属性(数据成员)和行为(成员函数)。通过类,我们可以创建具体的对象。 在C++中,类是通过关键字
class
定义的。一个类可以包含:
在C++中,类的定义包括类的声明和成员的实现。下面是一个类的基本定义示例:
#include <iostream>
using namespace std;
// 类的定义
class Person {
private:
// 私有成员变量
string name;
int age;
public:
// 构造函数,用于初始化对象
Person(string n, int a) {
name = n;
age = a;
}
// 公有成员函数,用于访问和修改对象的属性
void introduce() {
cout << "我叫 " << name << ",今年 " << age << " 岁。" << endl;
}
// 修改年龄的函数
void setAge(int newAge) {
age = newAge;
}
};
int main() {
// 创建对象
Person person1("小明", 20);
// 调用成员函数
person1.introduce();
// 修改年龄
person1.setAge(22);
// 再次调用成员函数
person1.introduce();
return 0;
}
类中的成员可以通过访问修饰符进行访问控制,常见的修饰符有:
public
:公有成员,外部代码可以访问。private
:私有成员,外部代码无法直接访问,只能通过类的公有函数访问。protected
:受保护成员,外部代码无法访问,但可以在派生类中访问。 构造函数:构造函数是用于初始化对象的特殊成员函数。构造函数的名字与类名相同,它在创建对象时自动调用。可以定义多个构造函数以支持不同的初始化方式。
析构函数:析构函数用于在对象销毁时执行清理工作,它的名称是类名前加一个波浪号 ~
,通常用于释放资源(如内存或文件句柄)。
类的一个重要特点是封装,它通过将数据和操作数据的函数放在一起,确保对象的内部状态只能通过定义好的接口访问和修改。通过封装,可以更好地控制程序的复杂性,确保对象内部状态的完整性。 在上面的例子中,
name
和age
是类的私有成员变量,不能被外部直接访问。它们只能通过introduce()
和setAge()
这样的公有成员函数来操作,从而保证了数据的安全性。
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。 数据成员作用域:类中的变量(成员变量)只能在类的成员函数内部或通过对象进行访问,具体取决于访问控制修饰符。 成员函数作用域:类中的成员函数可以访问类的所有成员(包括私有成员和保护成员),并且可以在对象创建后通过对象来调用。
在类中定义的成员函数和成员变量都有自己的作用域,且根据访问修饰符不同,具体作用范围有所不同。下面进一步说明作用域的影响:
类的成员变量的作用范围仅限于类内部,外部不能直接访问它,除非它是 public
成员或者通过 public
成员函数来访问。
class Test {
private:
int a; // 只能在类的内部访问
public:
int b; // 可以在类的外部通过对象访问
// 通过成员函数访问私有成员
void setA(int value) {
a = value;
}
int getA() {
return a;
}
};
在上面的代码中,a
是 private
成员,不能在类外部直接访问,而 b
是 public
成员,可以通过对象直接访问。而 a
需要通过 getA()
和 setA()
这样的 public
成员函数间接访问。
private
和 protected
成员。class Rectangle {
private:
int width, height;
public:
// 成员函数声明
void setValues(int, int);
int area();
};
// 成员函数定义
void Rectangle::setValues(int w, int h) {
width = w;
height = h;
}
int Rectangle::area() {
return width * height;
}
int main() {
Rectangle rect;
rect.setValues(5, 10);
cout << "矩形的面积是: " << rect.area() << endl;
return 0;
}
在这个例子中,setValues
和 area
是 public
成员函数,因此可以在 main
函数中通过 rect
对象调用它们。但 width
和 height
是 private
的,所以不能直接访问它们,只能通过 setValues
函数设置它们的值。
类的静态成员变量和静态成员函数有特殊的作用域,它们属于类本身,而不是某个具体的对象。也就是说,无需实例化对象即可访问类的静态成员。
class Test {
private:
static int count; // 静态成员变量
public:
Test() { count++; }
static int getCount() {
return count; // 静态成员函数访问静态成员变量
}
};
// 定义静态成员变量
int Test::count = 0;
int main() {
Test t1, t2, t3;
cout << "对象的数量是: " << Test::getCount() << endl; // 调用静态成员函数
return 0;
}
在上面的例子中,count
是一个静态成员变量,属于类 Test
,它的值在类的所有对象之间共享,并可以通过 Test::getCount()
这样的静态成员函数访问。需要注意的是,静态成员变量在类的外部进行定义。
在C++中,类还支持嵌套类,即一个类可以定义在另一个类的作用域中。嵌套类的作用域只限于包含它的类内部。
class Outer {
public:
class Inner {
public:
void display() {
cout << "这是嵌套类的成员函数" << endl;
}
};
};
int main() {
Outer::Inner obj; // 创建嵌套类的对象
obj.display();
return 0;
}
在这个例子中,Inner
类是 Outer
类的嵌套类,它的作用域仅限于 Outer
类内部,外部必须通过 Outer::Inner
来访问和创建对象。
类的实例化是指使用类的定义来创建具体的对象。类在C++中可以看作是一个模板,而实例化则是基于这个模板生成实际的对象,给对象分配内存并赋予它所定义的属性和行为。
在C++中,实例化类的语法非常简单,使用类名作为类型,然后定义对象即可:
class MyClass {
public:
void display() {
cout << "这是一个类的成员函数" << endl;
}
};
int main() {
MyClass obj; // 实例化类MyClass,创建一个对象obj
obj.display(); // 调用对象的成员函数
return 0;
}
在上面的例子中:
MyClass
是一个类。obj
是类 MyClass
的一个对象(实例)。obj.display()
调用了类的成员函数。实例化一个类通常包括以下步骤:
.
)来访问对象的成员函数和变量(如果成员变量是公有的)。当我们实例化类时,构造函数会被自动调用来初始化对象。C++允许通过不同的构造函数进行不同方式的初始化。
#include <iostream>
using namespace std;
class Rectangle {
private:
int width;
int height;
public:
// 构造函数
Rectangle(int w, int h) {
width = w;
height = h;
}
int area() {
return width * height;
}
};
int main() {
// 实例化对象并调用构造函数
Rectangle rect(5, 10);
cout << "矩形的面积: " << rect.area() << endl;
return 0;
}
在这个例子中,Rectangle
类定义了一个构造函数 Rectangle(int w, int h)
,当 rect
对象被实例化时,构造函数被调用,自动将宽度和高度设置为指定的值。
除了静态实例化对象(直接在栈上分配内存),C++还支持使用 new
操作符在堆上动态实例化对象。这样做的好处是可以在程序运行时动态分配内存,适用于更复杂的应用场景。
Rectangle rect(5, 10); // 静态实例化,内存在栈上分配
Rectangle* rect = new Rectangle(5, 10); // 动态实例化,内存在堆上分配
cout << "矩形的面积: " << rect->area() << endl;
delete rect; // 释放动态分配的内存
在动态实例化时,使用 new
操作符分配内存,rect
是指向 Rectangle
对象的指针,通过指针访问成员函数使用 ->
操作符。为了避免内存泄漏,动态分配的内存必须使用 delete
释放。
C++允许创建类对象的数组,这意味着可以一次实例化多个对象。对象数组的每个元素都是该类的一个实例。
class Circle {
public:
double radius;
Circle(double r) : radius(r) {}
double area() {
return 3.14 * radius * radius;
}
};
int main() {
// 实例化对象数组
Circle circles[3] = { Circle(1.0), Circle(2.0), Circle(3.0) };
// 遍历数组并调用成员函数
for (int i = 0; i < 3; ++i) {
cout << "Circle " << i + 1 << " 的面积是: " << circles[i].area() << endl;
}
return 0;
}
在这个例子中,我们创建了一个 Circle
对象数组,数组中的每个元素都是 Circle
类的实例,可以分别调用它们的成员函数。
class Point {
public:
int x, y;
Point(int x_val, int y_val) : x(x_val), y(y_val) {}
void display() {
cout << "Point(" << x << ", " << y << ")" << endl;
}
};
int main() {
Point p1(10, 20);
Point p2(30, 40);
p1.display(); // 输出 Point(10, 20)
p2.display(); // 输出 Point(30, 40)
}
在这个例子中,p1
和 p2
是 Point
类的两个不同实例,虽然它们共享同一个 display
函数,但输出不同的结果,因为函数操作的是各自对象的 x
和 y
成员变量。
当类的对象生命周期结束时(比如在函数结束时,或调用 delete
时),系统会自动调用类的析构函数来释放对象的资源。析构函数的名字是类名前面加 ~
号。通常用于释放动态分配的资源。
class MyClass {
public:
MyClass() {
cout << "构造函数被调用" << endl;
}
~MyClass() {
cout << "析构函数被调用" << endl;
}
};
int main() {
MyClass obj; // 实例化对象,调用构造函数
return 0; // 程序结束时,自动调用析构函数
}
在C++中,计算一个类的大小主要是指其对象在内存中占用的字节数。类的大小与类中包含的数据成员、继承关系、对齐方式以及可能的填充字节(padding)等因素有关。C++中的
sizeof
运算符可以用来计算类对象的大小。
一个类的大小由其成员变量占用的空间和潜在的填充字节组成。成员函数不影响类的大小,因为函数的代码并不会存储在每个对象的内存中。
#include <iostream>
using namespace std;
class MyClass {
int a;
double b;
};
int main() {
cout << "MyClass 的大小: " << sizeof(MyClass) << " 字节" << endl;
return 0;
}
在这个例子中,MyClass
有两个成员变量 int a
和 double b
。它们分别占用 4
和 8
字节。理论上,这个类的大小应该是 4 + 8 = 12
字节,但是由于编译器的对齐要求,实际的大小可能会是 16
字节。
C++中大多数系统对内存有特定的对齐要求,通常是 2
、4
或 8
字节。这意味着类中的每个成员变量的地址可能需要对齐到某个字节边界上,编译器可能会在变量之间插入“填充字节”以满足对齐要求。
#include <iostream>
using namespace std;
class MyClass {
char c;
int a;
};
int main() {
cout << "MyClass 的大小: " << sizeof(MyClass) << " 字节" << endl;
return 0;
}
在这个例子中,MyClass
有一个 char
类型成员 c
和一个 int
类型成员 a
。
char
通常占用 1
字节。int
通常占用 4
字节,但它可能需要对齐到 4
字节边界。因此,在 char c
后,编译器可能会插入 3
个填充字节,使 int a
从 4
字节对齐开始。这意味着类的实际大小可能是 8
字节而不是 5
字节。
在C++中,即使类中没有任何成员变量,空类的大小也不是 0
。为了确保每个对象都有唯一的地址,C++规定空类的大小为 1
字节。
class EmptyClass {};
int main() {
cout << "EmptyClass 的大小: " << sizeof(EmptyClass) << " 字节" << endl;
return 0;
}
结果通常会输出 1
字节。
在继承中,基类和派生类的大小通常会加在一起。如果基类有非静态成员,派生类会继承这些成员,派生类的大小等于基类的大小加上派生类新增的成员变量的大小。
class Base {
int a;
};
class Derived : public Base {
double b;
};
int main() {
cout << "Base 的大小: " << sizeof(Base) << " 字节" << endl;
cout << "Derived 的大小: " << sizeof(Derived) << " 字节" << endl;
return 0;
}
在这个例子中:
Base
类包含一个 int
,其大小通常为 4
字节(考虑到对齐,实际可能是 4
或 8
字节)。Derived
类继承了 Base
的 int a
,并添加了一个 double b
,double
通常占用 8
字节。最终的 Derived
类大小应该是 Base
的大小加上 double b
的大小。类中的虚函数会增加类的大小。虚函数的存在会使类包含一个虚函数表指针(vptr
),它指向该类的虚函数表。vptr
的大小通常是一个指针的大小,通常为 4
字节(在 32 位系统中)或 8
字节(在 64 位系统中)。
class MyClass {
int a;
virtual void func() {}
};
int main() {
cout << "MyClass 的大小: " << sizeof(MyClass) << " 字节" << endl;
return 0;
}
在这个例子中,尽管 func
函数不占用对象的内存空间,但是由于存在虚函数,MyClass
的对象会包含一个虚函数表指针(vptr
)。因此类的大小会比没有虚函数时稍大一些。
静态成员变量不属于某个具体的对象,它属于整个类,因此它不会影响类的大小。静态成员变量在类外部定义并且存储在静态区,所有对象共享这一个变量。
class MyClass {
static int a;
double b;
};
int MyClass::a = 10; // 静态成员在类外部定义
int main() {
cout << "MyClass 的大小: " << sizeof(MyClass) << " 字节" << endl;
return 0;
}
尽管 MyClass
有一个静态成员 a
,但它不会影响类对象的大小,只会计算非静态成员 b
的大小。
vptr
)会增加类的大小。#include <iostream>
using namespace std;
class Base {
int x;
};
class Derived : public Base {
char c;
double d;
static int s;
};
int Derived::s = 0;
int main() {
cout << "Base 的大小: " << sizeof(Base) << " 字节" << endl;
cout << "Derived 的大小: " << sizeof(Derived) << " 字节" << endl;
return 0;
}
在这个例子中:
Base
类只有一个 int
,其大小通常为 4
字节,但可能会有填充字节。Derived
类继承了 Base
的 int x
,并添加了一个 char
和一个 double
。由于对齐规则,类的实际大小可能比简单地将各成员的大小相加要大。vptr
)和对齐填充会影响类的大小。sizeof
运算符,你可以准确地知道类在内存中占用的大小。在C++中,类成员函数会隐式地接收一个指向当前对象的指针,称为
this
指针。this
指针用于访问当前对象的成员变量和成员函数。以下是this
指针的关键点和用法:
this
指针的定义this
,它是一个指向当前对象的指针。const
成员函数,this
的类型是ClassName*
,即指向类实例的指针。this
指针本身是只读的,无法修改this
指针使其指向其他对象。this
指针的使用场景this
指针用于区分成员变量和局部变量或参数名称相同的情况。this
指针在成员函数中返回当前对象的引用,以实现链式调用(如在操作符重载时)。this
指针可用于在类的成员函数内部将当前对象作为参数传递给其他函数。this
指针的例子class MyClass {
public:
int value;
MyClass(int value) {
// 使用 this 指针来区分参数和成员变量
this->value = value;
}
// 返回当前对象的引用(实现链式调用)
MyClass& setValue(int value) {
this->value = value;
return *this; // 返回当前对象的引用
}
// 打印成员变量的值
void printValue() const {
std::cout << "Value: " << this->value << std::endl;
}
};
说明:
this->value
用于区分成员变量value
与构造函数参数value
。setValue
函数返回*this
,即返回当前对象的引用,用于链式调用。printValue
函数中使用this
访问成员变量value
。const
成员函数中的this
指针const
成员函数,this
指针的类型是const ClassName*
,即指向const
类型对象的指针。意味着在const
成员函数中,无法通过this
修改当前对象的成员变量。例如:
class MyClass {
public:
int value;
// const成员函数,不能修改成员变量
void printValue() const {
std::cout << this->value << std::endl;
// this->value = 10;
// 错误:无法在const成员函数中修改成员变量
}
};
this
指针this
指针,不能访问类的非静态成员。例如:
class MyClass {
public:
static void staticFunction() {
// std::cout << this->value;
// 错误:this指针不存在,不能访问非静态成员
}
};
this
指针在运算符重载中的作用在运算符重载中,this
指针通常用于返回当前对象的引用,以支持链式操作。例如,在重载赋值运算符时,可以返回*this
来支持连续赋值:
class MyClass {
public:
int value;
// 重载赋值运算符
MyClass& operator=(const MyClass& other) {
if (this != &other) { // 避免自我赋值
this->value = other.value;
}
return *this; // 返回当前对象的引用
}
总结:
this
指针是指向当前对象的隐式指针,用于在成员函数中访问和操作对象的成员变量。它在区分局部变量和成员变量、实现链式调用以及避免自我赋值中起到了重要作用。对于const
成员函数,this
指针为const
指针,不能修改对象状态,而静态成员函数没有this
指针。
this
指针是一个隐式参数,传递给每个非静态成员函数。this
指针的存储位置与当前的函数调用栈和运行时有关,它通常会存储在寄存器或栈中,具体取决于编译器实现和CPU架构。this
指针不能为空,但它可以指向空对象。根据C++标准,this
指针在成员函数中总是指向当前对象。如果this
指针为空(即nullptr
),在访问对象的成员时会导致未定义行为,通常会导致程序崩溃或异常。例题:
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 (C)
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
Print()
是一个普通的成员函数,但它没有访问类的成员变量(即_a
),也没有依赖于当前对象的具体数据。因此,尽管 p
是 nullptr
,调用 p->Print()
并不会试图访问无效的对象内存。
this
指针在调用时是隐式传递的,但由于Print()
函数中没有使用与对象相关的成员变量,所以this
指针并未实际被解引用。
// 2.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 (B)
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
Print()
函数中有对成员变量_a的访问,那么this
指针将被解引用,导致程序崩溃或产生未定义行为。通过本文的学习,相信你已经对C++中的类和对象有了全面而深入的理解。类和对象不仅是C++面向对象编程的基础,更是现代软件开发不可或缺的工具。它们教会我们如何以更加抽象和模块化的方式思考问题,将复杂的系统分解成简单、可管理的部分。掌握这一技能,你将能够设计出更加灵活、健壮的软件系统,应对日益复杂的编程挑战。