C++语法学习(二) 函数、类、对象的概念和使用方法,面向对象编程思想、继承、多态等概念。
1. 类和对象 1.1 访问类成员 使用句点运算符访问成员:句点运算符(.)用于访问对象的属性。 1 2 firstMan.dateOfBirth = "1970" ; firstMan.IntroduceSelf ();
使用指针运算符(->)访问成员:如果对象是使用new 在自由存储区中实例化的,或者有指向对象的指针,则可使用指针运算符(->)来访问成员属性和方法。 1 2 3 4 Human* firstWoman = new Human (); firstWoman->dateOfBirth = "1970" ; firstWoman->IntroduceSelf ();delete firstWoman;
1.2 关键字public喝private public:有了对象后就可以获取它们。 private:指定哪些部分可以从外部访问,哪些部分不能。 1.3 声明友元 不能从外部访问类的私有数据成员和方法,但这条规则不适用于友元类和友元函数。要声明友元类或友元函数,可使用关键字friend
。 1 2 3 4 5 6 7 8 9 private : friend void DisplayAge (const Human& person) ; int age; string name;void DisplayAge (const Human& person) { cout << person.age << endl; }
将外部类指定为可信任的朋友。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 private : friend class Utility ; string name; int age;class Utility {public : static void DisplayAge (const Human& person) { cout << person.age << endl; } }; Utility::DisplayAge (firstMan);
1.4 共用体:一种特殊的数据存储机制 声明共用体:使用关键字union,再在这个关键字后面指定共用体名称,然后在大括号内指定其数据成员。 1 2 3 4 5 6 7 8 9 10 union UnionName { Type1 member1; Type2 menber2; ··· TypeN memberN; }; UnionName unionObject; unionObject.member2 = value;
共用体与类一样,可包含多个数据成员,但不同的是只能使用其中的一个。 与结构类似,共用体的成员默认也是公有的,但不同的是,共用体不能继承。 另外,将sizeof()用于共用体时,结果总是为共用体最大成员的长度,即便该成员并不处于活动状态。 2. 构造函数 构造函数是一种特殊的函数(方法),在根据类创建对象时被调用。与函数一样,构造函数也可以重载。
2.1 声明和实现构造函数 构造函数是一种特殊的函数,它与类同名且不返回任何值。 1 2 3 4 calss Human{public : Human (); }
构造函数可在类声明中实现,也可在类声明外实现。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Human {public : Human (){ } };class Human {public : Human (); }; Human::Human (){ }
使用构造函数:构造函数总是在创建对象时被调用,这让构造函数成为将类成员变量(int、指针等)初始化为选定值的理想场所。 可在不提供参数的情况下调用的构造函数被称为默认构造函数。默认构造函数是可选的。这种构造函数会创建成员属性,但不会将POD 类型(如int)的属性初始化为非零值。 重载构造函数 1 2 3 4 5 6 7 8 9 10 11 12 class Human {public : Human () { } Human (string humansName) { } };
没有默认构造函数的类:类没有默认构造函数,创建类对象时必须提供参数。 1 2 3 4 5 6 7 8 9 Human (string humansName, int humansAge){ name = humansName; age = humansAge; cout << "Overloaded constructor creates " << name; cout << "of age " << age << endl; }Human firstMan ("Adam" , 30 ) ;Human firstWoman ("Eve" , 28 ) ;
带默认值的构造函数参数:默认构造函数是调用时可不提供参数的构造函数,而并不一定是不接受任何参数的构造函数。 1 2 3 4 5 6 7 8 9 Human (string humansName, int humansAge = 25 ){ name = humansName; age = humansAge; cout << "Overloaded constructor creates " << name; cout << "of age " << age << endl; }Human firstMan ("Adam" ) ;Human firstWoman ("Eve" , 28 ) ;
包含初始化列表的构造函数 1 2 3 4 5 6 7 8 9 10 public : Human (string human = "Adam" , int humansAge = 25 ):name (human), age (humansAge) { cout << "Constructed " << name; cout << ", " << age << " years old" << endl; } }; Human adam;Human eve ("Eve" , 18 ) ;
也可使用关键字constexpr 将构造函数定义为常量表达式。 1 2 3 4 5 6 7 8 class Sample {const char * someString;public : constexpr Sample (const char * input) :someString(input) { } };
2.2 复制构造函数 类包含原始指针成员(char *等)时,务必编写复制构造函数和复制赋值运算符。 编写复制构造函数时,务必将接受源对象的参数声明为const 引用。 使用复制构造函数确保深复制(复制构造函数接受一个以引用方式传入的当前类的对象作为参数,分配了新的内存地址)。 浅复制并复制指向的内存单元,导致两个对象指向同一个内存单元,调用函数返回后,使用析构销毁变量,导致main函数中的对象指向无效内存。 3. 析构函数 析构函数在对象销毁时自动被调用。
3.1 声明和实现析构函数 析构函数看起来像一个与类同名的函数,但前面有一个腭化符号(~)。 1 2 3 class Human { ~Human (); }
这个析构函数可在类声明中实现,也可在类声明外实现。 1 2 3 4 5 6 class Human { ~Human (){ } }
1 2 3 4 5 6 7 8 9 10 class Human {public : ~Human (); } Human::~Human (){ }
使用析构函数 每当对象不再在作用域内或通过delete 被删除进而被销毁时,都将调用析构函数。这使得析构函数成为重置变量以及释放动态分配的内存和其他资源的理想场所。
析构函数不能重载,每个类都只能有一个析构函数。如果您忘记了实现析构函数,编译器将创建一个伪(dummy)析构函数并调用它。伪析构函数为空,即不释放动态分配的内存。 3.2 构造函数和析构函数的其他用途 不允许复制的类:要禁止类对象被复制, 可声明一个私有的复制构造函数。 只能有一个实例的单例类:使用单例的概念,它使用私有构造函数、私有赋值运算符和静态实例成员。 将关键字static 用于类的数据成员时,该数据成员将在所有实例之间共享。 将static 用于函数中声明的局部变量时,该变量的值将在两次调用之间保持不变。 将static 用于成员函数(方法)时,该方法将在所有成员之间共享。 禁止在栈中实例化的类:栈空间通常有限。如果您要编写一个数据库类,其内部结构包含数TB 数据,可能应该禁止在栈上实例化它,而只允许在自由存储区中创建其实例。为此,关键在于将析构函数声明为私有的。
1 2 3 4 5 6 7 8 9 10 11 class MonsterDB {private : ~MonsterDB (); }int main () { MonsterDB myDatabase; return 0 ; }
4. 封装 封装的意义一 将属性和行为作为一个整体,表现生活中的事物 将属性和行为加以权限控制 1 2 3 4 class 类名{ 访问权限: 属性 / 行为 };
封装的意义二:类在设计时,可以把属性和行为放在不同的权限下,加以控制。访问权限有三种(访问修饰符): public 公共权限 protected 保护权限 private 私有权限 5. 继承 5.1 继承和派生
image-20240402162226768 派生语法 1 2 3 4 5 6 7 8 9 class Base { };class Derived : access-specifier Base{ };
1 2 3 4 5 6 7 8 class Fish { };class Carp :public Fish{ };
访问限定符protected:需要让基类的某些属性能在派生类中访问,但不能在继承层次结构外部访问,可使用关键字protected。 与public和private一样,protected也是一个访问限定符。将属性声明为protected时,相当于允许派生类和友元类访问它,但禁止在继承层次结构外部包括main访问它。
基类初始化–向基类传递参数 1 2 3 4 5 6 7 8 9 10 11 12 13 class Base {public : Base (int someNumber){ } };class Derived :public Base{public : Derived (): Base (25 ){ } };
派生类中覆盖基类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Base {public : void DoSomething () { } };class Derived :public Base { public : void DoSomething () { } };
构造顺序 基类对象在派生类对象之前被实例化,先构造派生类对象的基类部分(成员属性)。 实例化基类部分和派生类部分时,先实例化成员属性,再调用构造函数。 顺序:初始化基类成员属性(调用属性的构造函数),再调用基类的构造函数;初始化派生类成员属性,再调用派生类的构造函数。 析构顺序 与构造顺序相反。 顺序:调用派生类的析构函数,再调用成员属性的析构函数;再调用基类的析构函数,最后调用基类成员属性的析构函数。 私有继承:指定派生类的基类时使用关键字private,私有继承意味着在派生类的实例中,基类的所有公有成员和方法都是私有的—-不能从外部访问。 1 2 3 4 5 6 7 8 9 class Base { };class Derived : private Base { };
保护继承:保护继承不同于公有继承之处在于,声明派生类继承基类时使用关键字protected。 保护继承与私有继承的类似之处如下:
它也表示has-a 关系; 它也让派生类能够访问基类的所有公有和保护成员; 在继承层次结构外面,也不能通过派生类实例访问基类的公有成员。 不同之处:
在保护继承层次结构中,子类的子类(即Derived2)能够访问Base 类的公有和保护成员。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Base { };class Derived : protected Base { };class Derived2 : protected Derived { };
多继承 1 2 3 4 class Derived : access-specifier Base1, access-specifier Base2 { };
使用final禁止继承:被声明为final 的类不能用作基类 6. 多态 多态(Polymorphism)是面向对象语言的一种特征,让您能够以类似的方式处理不同类似的对象
使用虚函数实现多态行为:通过使用关键字virtual,可确保编译器调用覆盖版本。将派生类对象视为基类对象。 1 2 3 4 5 6 7 8 class Base {virtual ReturnType FunctionName (Parameter List) ; };class Derived {ReturnType FunctionName (Parameter List) ; };
对于使用new 在自由存储区中实例化的派生类对象,如果将其赋给基类指针,并通过该指针调用delete,将不会调用派生类的析构函数。这可能导致资源未释放、内存泄露等问题。要避免这种问题,可将析构函数声明为虚函数。 1 2 3 4 5 6 class Base {public :virtual ~Base () {}; };
虚函数工作原理—-虚函数表:编译器为实现了虚函数的基类和覆盖了虚函数的派生类分别创建一个虚函数表(VFT)。 不能实例化的基类被称为抽象基类,这样的基类只有一个用途,那就是从它派生出其他类。在C++中,要创建抽象基类,可声明纯虚函数。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class AbstractBase {public :virtual void DoSomething () = 0 ; };class Derived : public AbstractBase {public :void DoSomething () { cout << "Implemented virtual function" << endl; } };
在继承层次结构中,继承多个从同一个类派生而来的基类时,如果这些基类没有采用虚继承,将导致二义性。这种二义性被称为菱形问题(Diamond Problem)。 使用限定符 override 来核实被覆盖的函数在基类中是否被声明为虚函数。 1 2 3 4 void Swim () const override { cout << "Tuna swims!" << endl; }
使用 final 来禁止覆盖函数。 1 2 3 4 5 void Swim () override final { cout << "Tuna swims!" << endl; }
不能将复制构造函数声明为虚函数 7. this指针 关键字this 包含当前对象的地址,其值为&object
。当您在类成员方法中调用其他成员方法时,编译器将隐式地传递this 指针—函数调用中不可见的参数。 调用静态方法时,不会隐式地传递this 指针,因为静态函数不与类实例相关联,而由所有实例共享。 要在静态函数中使用实例变量,应显式地声明一个形参,并将实参设置为this 指针。 sizeof()
用于类及其对象时,结果相同,因为类占用的字节数在编译阶段就已经确定。1 2 3 4 5 6 private : int age; bool gender; MyString name;
8. 运算符类型与运算符重载 8.1 单目运算符 单目运算符只对一个操作数进行操作。实现为全局函数或静态成员函数的单目运算符的典型定义如下: 1 2 3 return_type operator operator_type (parameter_type) { }
作为类成员(非静态函数)的单目运算符没有参数,因为它们使用的唯一参数是当前类实例(*this),如下所示: 1 2 3 return_type operator operator_type () { }
单目运算符的类型:可以重载(或重新定义)的单目运算符: ++ 递增 & 取址 —- 递减 ~ 求反 * 解除引用 + 正 -> 成员选择 - 负 ! 逻辑非 转换运算符 转换为其他类型
递增&递减 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 Date& operator ++ () { ++VARIABLE return *this ; } Date operator ++ (int ) { Date copy (*this ) ; VARIABLE++ return copy; } Date& operator -- () { --VARIABLE return *this ; } Date operator -- (int ) { Date copy (*this ) ; VARIABLE- return copy; }
8.2 转换运算符 当类不支持当前的运算符是,将类的对象的内容转换成cout
能够接受的类型。例如const char*
,只需添加一个返回const char*
的运算符。 1 2 3 operator const char *(){ }
8.3 解除引用运算符(*)和成员选择运算符(->) 解除引用运算符(*)和成员选择运算符(->)在智能指针类编程中应用最广。智能指针是封装常规指针的类,旨在通过管理所有权和复制问题简化内存管理。在有些情况下,智能指针甚至能够提高应用程序的性能。 8.3 双目运算符 对两个操作数进行操作的运算符称为双目运算符。以全局函数或静态成员函数的方式实现的双目运算符的定义如下: 1 return_type operator_type (parameter1, parameter2) ;
以类成员的方式实现的双目运算符只接受一个参数,其原因是第二个参数通常是从类属性获得的。以类成员的方式实现的双目运算符的定义如下: 1 return_type operator_type (parameter) ;
双目运算符的类型 , 逗号 < 小于 != 不等于 << 左移 % 求模 <<= 左移并赋值 %= 求模并赋值 <= 小于或等于 & 按位与 = 赋值、复制赋值和移动赋值 && 逻辑与 == 等于 &= 按位与并赋值 > 大于 * 乘 >= 大于或等于 *= 乘并赋值 >> 右移 + 加 >>= 右移并赋值 += 加并赋值 ^ 异或 - 减 ^= 异或并赋值 -= 减并赋值 | 按位或 ->* 指向成员的指针 |= 按位或并赋值 / 除 || 逻辑或 /= 除并赋值 [] 下标运算符
双目加法与双目减法运算:与递增/递减运算符类似,如果类实现了双目加法和双目减法运算符,便可将其对象加上或减去指定类型的值。
重载等于运算符(==)和不等运算符(!=)
由于还没有定义等于运算符(==),编译器将对这两个对象进行二进制比较
,并仅当它们完全相同时才返回true。对于包含简单数据类型的类,这种二进制比较是可行的。然而,如果类有一个非静态字符串成员
,它包含字符串值(char *)
,则比较结果可能不符合预期。在这种情况下,对成员属性进行二进制比较时,实际上将比较字符串指针
,而字符串指针并不相等(即使指向的内容相同),因此总是返回false。为了解决这种问题,可定义比较运算符。
1 2 3 4 5 6 7 8 9 10 11 bool operator == (const ClassType& compareTo) { }bool operator != (const ClassType& compareTo) { }
下标运算符:下标运算符能够像访问数组那样访问类,其典型语法如下 1 return_type& operator [] (subscript_type& subscript);
编写封装了动态数组的类(如封装了char* buffer 的MyString)时,通过实现下标运算符,可轻松地随机访问缓冲区中的各个字符:
1 2 3 4 5 6 7 8 class MyString { public : char & operator [] (int index) { } };
8.4 函数运算符 operator() operator()让对象像函数,被称为函数运算符。函数运算符用于标准模板库(STL)中,通常是STL算法中,其用途包括决策。根据使用的操作数数量,这样的函数对象通常称为单目谓词或双目谓词。 8.5 移动构造函数和移动赋值运算符 ** 移动构造函数和移动赋值运算符乃性能优化功能,属于C++11 标准的一部分,旨在避免复制不必要的临时值(当前语句执行完毕后就不再存在的右值)。对于那些管理动态分配资源的类,如动态数组类或字符串类,这很有用。 声明移动构造函数和移动赋值运算符: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Sample {private : Type* ptrResource;public : Sample (Sample&& moveSource){ ptrResource = moveSource.ptrResource; moveSource.ptrResource = NULL ; } Sample& operator = (Sample&& moveSource){ if (this != &moveSource){ delete [] ptrResource; ptrResource = moveSource.ptrResource; moveSource.ptrResource = NULL ; } } Sample (); Sample (const Sample& copySource); Sample& operator = (const Sample& copySource); };
移动构造函数和移动赋值运算符的不同之处在于,输入参数的类型为Sample&&。另外,由于输入参数是要移动的源对象,因此不能使用const 进行限定,因为它将被修改。返回类型没有变,因为它们分别是构造函数和赋值运算符的重载版本。
8.5 用户定义的字面量 自定义字面量
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 Temperature k1 = 32.15 _F Temperature k2 = 0.0 _C ReturnType operator "" YourLiteral (ValueType value) { }#include <iostream> using namespace std;struct Temperature { double Kelvin; Temperature (long double kelvin) : Kelvin (kelvin) {} }; Temperature operator "" _C(long double celsius){ return Temperature (celsius + 273.15 ); } Temperature operator "" _F(long double fahrenheit){ return Temperature ((fahrenheit - 32 ) * 5 / 9 + 273.15 ); }int main () { Temperature t1 = 36.5 _C; Temperature t2 = 98.6 _F; cout << "Temperature t1 is " << t1.Kelvin << " Kelvin" << endl; cout << "Temperature t2 is " << t2.Kelvin << " Kelvin" << endl; return 0 ; }
image-20240602161047289 9. 类型转换运算符 9.1 C++类型转换运算符 使用语法 1 destination_type result = cast_operator <destination_type> (object_to_cast);
static_cast 使用static_cast 可将指针向上转换为基类类型,也可向下转换为派生类型。
1 2 Base* objBase = new Derived (); Derived* objDer = static_cast <Derived*>(objBase);
dynamic_cast 与静态类型转换相反,动态类型转换在运行阶段(即应用程序运行时)执行类型转换。可检查dynamic_cast 操作的结果,以判断类型转换是否成功。
1 2 3 4 5 Base* objBase = new Derived (); Derived* objDer = dynamic_cast <Derived*>(objBase);if (objDer) objDer->CallDerivedFunction ();
reinterpret_cast 将一种对象类型转换为另一种,不管它们是否相关。这种类型转换实际上是强制编译器接受static_cast 通常不允许的类型转换,通常用于低级程序。
1 2 Base* objBase = new Base (); Unrelated* notRelated = reinterpret_cast <Unrelated*>(objBase);
const_cast const_cast的大部分使用主要是将常量指针转换为常指针。常量指针指向的空间的内容不允许被修改,但是使用const_cast进行强制转换就可以修改。