目录
part 1 - 基础内容
类和封装的概念
类的基础写法
part 2 - 权限相关
类的权限
友元 - friend
内部类
part 3
实体化对象
类中的静态成员
静态成员变量
静态成员函数
const修饰的静态成员变量和普通的静态成员变量的区别
类的存储
类大小的计算
空类也要占一个字节
part4
this指针
要点概述
简要总结
const成员函数
使用规则
const修饰的是this指针
part5
匿名对象
匿名类
需要注意的小点
part 1 - 基础内容
类和封装的概念
在 C++ 中,类(Class)是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础。类是一种用户自定义的数据类型,它是一种抽象数据类型(Abstract Data Type,ADT)的实现方式之一。类允许程序员定义一组数据成员和成员函数,以封装数据和行为。类实例化出来的变量叫对象。
封装本质上是一种思想,可以让用户更方便使用类。比如,电脑提供给用户的就只有开关机键、鼠标、键盘,显示器等组件。但实际上电脑真正工作的却是CPU、显卡、内存等这样的一些元件。对于电脑的使用者而言,不需要关心其内部的原理,比如CPU内部是如何工作的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。
上述的这个例子就很好的展示了封装:把这些精密的元件有序的组装起来,然后只提供有限的几个接口用以完成设计好的步骤。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
类的基础写法
class className
{// 类的主体:由成员函数和成员变量组成
}; // 这里和结构体一样,都要有分号
用法理解:
- class为定义类的关键字,ClassName为类的名字,{}中为类的主体。注意类定义结束时后面分号不能省略。
- 类主体中内容称为类的成员:类中的变量称为类的属性或成员变量,类中的函数称为类的方法或者成员函数。
- 写法优化:使用构造类型时,可以直接类名一个对象,不用再加struct、class等。
part 2 - 权限相关
类的权限
在 C++ 中,类的成员可以有不同的访问权限。访问权限决定了类的成员对外部世界(类外部)和派生类(子类)的可见性和可访问性。不过需要注意:
访问限定符只在编译时起作用。因为在编译阶段编译器会检查代码中的访问权限,并将访问控制信息转换成目标代码。一旦代码编译成功,访问限定符的作用就已经完成,不再影响程序的运行时行为。这样做可以提高代码执行效率并确保访问行为的正确性。
其中,与访问权限相对应的是三种访问限定符:public、protected 和 private。
public:public 成员对所有代码可见,包括类的外部和派生类。这意味着任何地方都可以直接访问该成员。
protected:protected 成员对类的外部是不可见的,但对派生类是可见的。这意味着只有类的内部和派生类、友元等才可以直接访问 protected 成员,类的外部代码不能直接访问。
private:private 成员对类的外部和派生类都是不可见的,只有类的内部、友元等可以直接访问 private 成员。
下面是一个示例代码来演示这三种访问权限的区别:
class MyClass { public:int publicVar; // public 成员 protected:int protectedVar; // protected 成员 private:int privateVar; // private 成员 };class DerivedClass : public MyClass /*派生类*/ { public:void accessBaseMembers() {publicVar = 1; // 可以直接访问 public 成员protectedVar = 2; // 可以直接访问 protected 成员// privateVar = 3; // 不能直接访问 private 成员} };int main() {MyClass obj;obj.publicVar = 1; // 可以直接访问 public 成员// obj.protectedVar = 2; // 不能直接访问 protected 成员// obj.privateVar = 3; // 不能直接访问 private 成员return 0; }
友元 - friend
在C++中,类的友元(Friend of a class)是一种特殊的访问权限,允许其他类或函数访问该类的私有成员和保护成员。通常情况下,只有该类的成员函数可以直接访问其私有成员和保护成员,但有时候需要允许其他类或函数访问这些成员,这时可以使用友元。
友元的声明方式是在要授权访问的类或函数前加上`friend`关键字。这样,该类或函数就成为了被声明为友元的类的友元,从而具有访问其私有成员和保护成员的权限。
以下是一个简单的示例,演示了如何在C++中使用友元:
#include <iostream>// 声明一个类
class MyClass {
private:int privateData;public:MyClass() : privateData(0) {}// 友元函数的声明friend void friendFunction(const MyClass& obj);// 友元类的声明friend class FriendClass;
};// 定义一个友元函数,可以访问私有成员privateData
void friendFunction(const MyClass& obj) {std::cout << "Friend function accessing privateData: " << obj.privateData << std::endl;
}// 定义一个友元类,可以访问私有成员privateData
class FriendClass {
public:void accessPrivateData(const MyClass& obj) {std::cout << "Friend class accessing privateData: " << obj.privateData << std::endl;}
};int main() {MyClass obj;friendFunction(obj);FriendClass friendObj;friendObj.accessPrivateData(obj);return 0;
}
在上面的例子中,`friendFunction`是一个友元函数,`FriendClass`是一个友元类,它们都可以访问`MyClass`中的私有成员`privateData`。
不过虽然友元在某些情况下很有用,但不要过度使用,因为它们会破坏封装性,并且使代码更加复杂。只有在确实需要其他类或函数直接访问私有成员或保护成员时,才应该使用友元。
- 注意事项:
- 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类。
- 友元函数不能用const修饰。因为友元函数不是类的成员函数,因此它没有隐式的this指针,所以也就无法使用const修饰符来限制对对象的修改。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数。
- 友元函数的调用与普通函数的调用原理相同。
- 友元的关系是单向的,不具有交换性。当一个类A将另一个类B声明为友元时,类B可以访问类A的私有成员,但类A无法访问类B的私有成员,除非类B也将类A声明为友元。
- 友元关系不能传递。例如C是B的友元, B是A的友元,但C却不是A的友元。
- 友元关系不能继承,即友元关系不会在派生类中自动继承。也就是说,当一个类派生出子类时,子类不能自动访问父类中的友元。
- 友元类的所有成员函数都可以是另一个类的友元函数,都可以肆意的访问另一个类中的非公有成员。
内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
在使用C++内部类时,有一些注意事项:
- 内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
- 内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
其实本质上讲,内部类和外部类就是两个单独的大小类。
所以内部类对外部类而言就和普通成员一样,这就使得内部类可以访问外部类的所有成员。而外部类对内部类而言是一个单独的类,所以外部类访问内部类的东西要遵循权限的限制。
而且外部的大类实例化对象时,内部的类并不会跟着实体化。(而且外部类的构造也不带内部类)内部的类需要作用域限定符单独实体化。
总的来说,内部类和外部内就是两个单独的类,只是作用域有所不同。
part 3
实体化对象
类是用来描述对象的,类的实体化其实就是创建对象,但关于类的实体化要注意下面这几点:
- 类内的成员变量只是声明,并不是实际的定义,因此也就没有分配内存空间。只有当实例化对象时,才会根据构造函数等实例化出对象,此时类的成员变量才算是有了实体。
- 定义一个类时并没有为它分配实际的内存空间。这一点和结构体类似,只有当实例化出对象的时候,才算是为这个对象分配了空间。
- 在实体化对象时,不单独为成员函数以及静态成员分配空间,即类内的static和函数,都是公用的,具有相同的入口。实体化对象时不单独为其分配空间。具体细节可以参考一下这篇博客:关于C++中类在实体化时不同成员的内存分配问题_小白麋鹿的博客-CSDN博客
- 类的空指针对象也是可以正常访问类的成员函数的,但不一定可以正常访问类的成员变量(取决于编译器是否优化)。
类中的静态成员
在C++中,类中的静态成员是与类本身关联而不是与类的实例对象关联的成员。静态成员被所有该类的对象所共享,无论有多少个类的实例,静态成员只有一个副本存在。它们在内存中位于数据区而不是每个对象的堆栈中。
类中的静态成员可以分为两种类型:静态数据成员和静态成员函数
静态成员变量
静态成员变量用于表示类范围内的共享数据。必须在类的外部(不能在函数内)进行定义,以便分配存储空间。定义时要在前面加上类名和作用域解析运算符(::)。代码示例如下:
class MyClass {
public:static int staticData; // 静态数据成员声明
};int MyClass::staticData = 0; // 静态数据成员定义并初始化
静态成员函数
静态成员函数是类的静态函数,与类的实例无关,只能访问静态数据成员和其他静态成员函数。静态成员函数没有this指针,因此不能访问非静态成员变量(也就不能用const修饰)。它们通过类名调用,而不是通过对象。例如:
class MyClass {
public:static int staticData; // 静态数据成员声明static void staticFunction() { // 静态成员函数定义// 可以访问静态数据成员staticData = 10;// 不能访问非静态成员变量// int x = memberVariable; // 错误}
};int MyClass::staticData = 0; // 静态数据成员定义并初始化int main() {MyClass::staticFunction(); // 调用静态成员函数return 0;
}
总结:静态成员的使用场景包括在所有类实例之间共享数据、作为辅助函数处理类范围内的任务等。而且,静态成员可以通过类名直接访问,无需创建类的对象。
const修饰的静态成员变量和普通的静态成员变量的区别
- const修饰的静态成员变量保存在常量区,而普通的静态成员变量保存在静态全局区
- const修饰的静态成员变量在类内定义并完成初始化,而普通的静态成员变量在类内声明类外定义
类的存储
类大小的计算
一个类的对象中包含了该类的所有成员变量和成员函数。成员变量是用于存储对象数据的,而成员函数是类的行为和操作。一个类的大小是由其非静态成员变量决定的。
在计算一个类的大小时,需要考虑以下几点:(暂时不考虑继承和虚函数等问题)
非静态成员变量:每个成员变量所占的内存大小取决于其类型,类的成员变量可以是基本数据类型、自定义类型(例如结构体或类)、指针等。
对齐方式:为了优化内存访问,编译器可能会在成员变量之间插入填充字节,以确保对齐。对齐方式可以通过编译器的选项或指令进行控制,一般而言,对齐的大小是成员变量中占用最大字节数的数据类型大小。
所以,类的大小计算并不考虑成员函数与静态成员变量的大小,只考虑非静态成员变量以及对齐规则。可以理解为和C语言的结构体计算方法类似。(因为C语言的结构体中是不能放静态成员的)
而至于对齐规则,参考结构体的对其规则:
- 以所占字节数最大的基本类型大小为单位开辟内存。
- 第一个成员必须在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到指定对齐数的整数倍地址处。而这个对齐数就是所有变量类型最大者与默认对齐参数取最小。当没有默认对齐数时,对齐数就是成员自身的大小。(VS中默认的对齐数为8)
- 结构体总大小一定为指定对齐数的整数倍。
- 成员中出现了数组时,数组可以看成n个变量。
- 如果嵌套了结构体,可以把其展开看待。
空类也要占一个字节
在C++中,空类(不含任何成员变量和成员函数)通常也会占用一个字节的内存空间。这是因为C++编译器在为每个类创建对象时,为了确保对象的地址是唯一的,需要为其分配至少一个字节的内存空间。
如果空类不占用任何内存,那么多个空类对象的地址可能会相同,这将导致无法区分它们。因此,为了确保空类的对象地址唯一,编译器通常会为其分配一个字节的内存空间。
需要注意的是,编译器在为空类分配内存时可能还会包含其他的调整或填充字节,以满足特定的对齐要求。这些额外的字节通常不是由类的成员决定的,而是由编译器或编译选项决定的。因此,空类的实际大小可能会大于一个字节,但至少会占用一个字节。
part4
this指针
要点概述
C++中的this指针是一个特殊的指针,它指向当前对象的地址。每个非静态成员函数都有一个隐藏的this指针参数,用于指向调用该函数的对象。这个指针直接可以在函数内部使用,以访问和操作当前对象的成员变量和成员函数。(静态成员函数是属于类而不是类的实例的函数。因为它们不依赖于特定的类对象,所以在静态成员函数内部,没有隐含的this指针)
当使用对象调用成员函数时,编译器会自动将该对象的地址作为参数传递给this指针。可以使用this指针来引用当前对象的成员变量和成员函数,以及访问其他该对象的操作。
在C++条件中,类的不同成员函数中,它们的`this`指针是不同的,但它们都指向同一个对象的地址。每个成员函数在被调用时,都会有一个隐含的指向当前对象的指针,即`this`指针。这个`this`指针是一个常量指针,不能被修改,它指向调用该成员函数的对象。
不同成员函数的`this`指针值是相同的,因为它们都指向调用该函数的对象。但是,每个成员函数都有自己的函数作用域,它们的`this`指针是在不同的函数调用时生成的。这意味着虽然它们的值相同(指向同一个对象),但它们的却并不是同一个this指针。
示例代码说明这一点:
#include <iostream>class MyClass {
public:void func1() {std::cout << "Address of this in func1: " << this << std::endl;}void func2() {std::cout << "Address of this in func2: " << this << std::endl;}
};int main() {MyClass obj;obj.func1();obj.func2();return 0;
}
输出可能是类似于以下的结果(实际结果可能会有所不同):
Address of this in func1: 0x7ffd42f72b40
Address of this in func2: 0x7ffd42f72b40
可以看到,在`func1`和`func2`中,`this`指针的值是相同的,都是指向`obj`对象的地址,但它们的地址可能是不同的。
在C++中,每当调用类的成员函数时,编译器会为该成员函数创建一个隐式的`this`指针变量。这个`this`指针变量的作用域就是当前函数,即它只在当前函数中有效,在函数执行完毕后就会被销毁。
当调用另一个成员函数时,编译器会再次创建一个新的`this`指针变量,它将在新的函数作用域内生效。每个成员函数都有自己的`this`指针,它们在函数调用时被创建和销毁,而且都指向调用该函数的对象。
这种机制使得在成员函数内部可以访问对象的成员变量和其他成员函数,因为通过`this`指针可以准确定位到对象的地址。同时,这也使得在类的不同成员函数中能够使用同名的参数和局部变量,因为它们的作用域是不同的函数。
简要总结
- this指针是一个隐式的参数,不需要手动传参,可以直接使用。每当调用类的成员函数时,编译器会为该成员函数创建一个隐式的`this`指针变量。
- this指针的本质是一个常量指针,因此在使用this指针时,只能进行数据的访问,不能对this指针进行修改。
- 每个成员函数的this指针相互独立的,但它们的值,即执行的地址是相同的,也就是对象本身的地址。
- 每个成员函数this指针的作用域就是当前的成员函数,生命周期就是从成员函数栈帧的创建到销毁。
- 所以每个this指针都只能在当前的成员函数中使用。
- this指针本质上只是“成员函数”的一个隐式形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中并不存储this指针。
- this指针和其它参数一样,都是在栈区开辟内存空间,存放在栈帧中的。
- this指针在成员函数内部指向当前调用该成员函数的对象实例。因此,通常情况下,this指针不能为空。但有一种特殊情况下this指针可以为空:当调用一个成员函数时,如果该成员函数是虚函数,并且通过一个空指针调用,那么this指针会为空。
const成员函数
使用规则
在C++中,成员函数可以被声明为`const`成员函数,这表示该函数承诺不会修改调用它的对象的状态。使用`const`关键字修饰的成员函数在其参数列表后面加上`const`关键字,表示它是一个常量成员函数。
注意事项
- 在cosnt成员函数内,不能对类内成员变量做修改,除非这些成员变量声明为mutable(使用 mutable 关键字可以使成员变量即使在常量成员函数内也可以被修改)。
- const成员函数可以被常量对象调用(即const修饰的对象),但常量对象只能调用常量成员函数,不允许调用非常量成员函数。
- const成员函数也可以被非常量对象调用。对于非常量对象,既可以调用const成员函数,也可以调用非const成员函数。
- 如果同时存在const成员函数和non-const成员函数,const对象会优先匹配const成员函数。(详见Effective C++,p19)
- 注意,常量性也是可以作为函数重载的标准的。
以下是常量成员函数的示例:
class MyClass {
public:int getValue() const {// 不能修改成员变量,只能读取// value = 10; // 错误,不允许在常量成员函数中修改成员变量return value;}void setValue(int val) {value = val;}private:int value;
};int main() {const MyClass obj1; // 常量对象MyClass obj2;obj2.setValue(5);std::cout << "obj2 value: " << obj2.getValue() << std::endl; // Output: obj2 value: 5// obj1是常量对象,只能调用常量成员函数// obj1.setValue(10); // 错误,常量对象不能调用非常量成员函数std::cout << "obj1 value: " << obj1.getValue() << std::endl; // Output: obj1 value: 5return 0;
}
在上述示例中,`getValue()`函数被声明为常量成员函数,因此常量对象`obj1`可以调用它,但不能调用`setValue()`函数。非常量对象`obj2`可以调用`setValue()`和`getValue()`函数。
const修饰的是this指针
在C++中,const成员函数的`const`修饰的是隐含的`this`指针,即它表示成员函数内部不能通过this指针修改所指向的对象的成员变量。当我们将成员函数声明为const成员函数时,编译器会在内部将this指针声明为指向const对象的常量指针(const T* this)。这样做可以确保在const成员函数中,不能通过`this`指针修改对象的成员变量。当然,如果成员变量被声明为`mutable`,即使在const成员函数中,也可以通过`this`指针修改这些`mutable`成员变量。
part5
匿名对象
在C++中,匿名对象是指创建一个没有被命名的临时对象,它没有被赋予一个明确的变量名或标识符。匿名对象通常用于临时的、一次性的操作,例如作为函数的参数或返回值。它们在创建后会在当前表达式结束时立即被销毁,所以它们只在创建的表达式范围内有效。
创建匿名对象的语法非常简单,只需在类名后添加一对小括号即可调用类的构造函数,而不给对象赋予一个变量名。下面是一个简单的例子:
#include <iostream>class MyClass
{
public:MyClass() {std::cout << "Constructor called!" << std::endl; //调用构造}~MyClass() {std::cout << "Destructor called!" << std::endl; //调用析构}
};int main()
{// 创建匿名对象MyClass(); // 匿名对象在此处被创建和销毁// 当然,我们也可以创建一个具名对象MyClass namedObject; // 具名对象,有一个变量名"namedObject"return 0;
}
在上面的例子中,我们在`main()`函数中创建了一个匿名对象`MyClass()`和一个具名对象`namedObject`。匿名对象会在声明的时候立即执行构造函数,然后在表达式结束时自动调用析构函数销毁。因此,你会看到输出的内容中构造函数和析构函数的调用顺序。
需要注意的是,由于匿名对象没有变量名,所以无法在之后的代码中再次引用或使用它。如果需要在后续代码中继续使用对象,就需要创建一个具名对象,赋予它一个变量名。
匿名类
在C++中,匿名类(无名的类)是指没有显式指定类名的类定义,通常用于一次性的场景,例如作为某个类的成员或者作为函数的返回值。匿名类可以在声明的同时定义,没有名字的类对象只能在定义的作用域内使用。
注意事项:
- 匿名类的实例只能在定义所在的作用域内使用,超出作用域后无法访问。
- 匿名类无法在其他地方定义新的对象,因为没有类名可以使用。
- 由于匿名类没有类名,所以无法在其内部定义构造函数、析构函数、友元函数等。
- 匿名类的成员函数必须在类的定义内部实现,无法在外部实现。
示例如下:
class OuterClass {
public:OuterClass() {// 无法在其他地方定义新的匿名类对象// Error: 'InnerClass' does not name a type// InnerClass obj;}void FunctionWithAnonymousClass() {// 定义并使用匿名类class {public:void SomeFunction() {// 匿名类的成员函数必须在类定义内部实现// do something}} obj;obj.SomeFunction();}
};int main() {OuterClass obj;obj.FunctionWithAnonymousClass();return 0;
}
虽然匿名类有一些局限性,但在某些特定场景下,它可以提供更加简洁的代码结构和更好的封装性。如果需要在多个地方使用相同的类逻辑,还是建议使用具名的类定义。
需要注意的小点
- C++兼容C语言,结构用法可以继续使用
- C++中,类内不能进行赋值操作,但可以对成员变量初始化。
- C++在类内没有指定权限的部分默认都是private的,而结构体中默认都是public的。
- 如果类的成员函数是在类内同时声明和定义的,那么默认是内联函数。(不过编译器最终是否将其视作内联就不一定了)如果是类内声明,类外定义的,就不是内联。不过具体使用时并不用太可以管这个,因为编译器的优化是很复杂的。
- 类的成员函数最好是类内声明,类外定义。主要是为了实现代码的分离和模块化,以及解决头文件的循环包含问题。将类的成员函数在类内部声明、在类外部定义的做法是为了提高代码的可维护性、可读性和编译效率,同时遵循面向对象的封装原则。这也是 C++ 常见的编程实践。
- 类定义了一个新的作用域,类中所有成员都在类的作用域中。在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域