C++语法学习(二)

C++语法学习(二)


函数、类、对象的概念和使用方法,面向对象编程思想、继承、多态等概念。

1. 类和对象

1.1 访问类成员
  1. 使用句点运算符访问成员:句点运算符(.)用于访问对象的属性。
1
2
firstMan.dateOfBirth = "1970";
firstMan.IntroduceSelf();
  1. 使用指针运算符(->)访问成员:如果对象是使用new 在自由存储区中实例化的,或者有指向对象的指针,则可使用指针运算符(->)来访问成员属性和方法。
1
2
3
4
Human* firstWoman = new Human();
firstWoman->dateOfBirth = "1970";
firstWoman->IntroduceSelf();
delete firstWoman;
1.2 关键字public喝private
  1. public:有了对象后就可以获取它们。
  2. private:指定哪些部分可以从外部访问,哪些部分不能。
1.3 声明友元
  1. 不能从外部访问类的私有数据成员和方法,但这条规则不适用于友元类和友元函数。要声明友元类或友元函数,可使用关键字friend
1
2
3
4
5
6
7
8
9
private:
friend void DisplayAge(const Human& person);
int age;
string name;

// 可以访问到私有变量中的 age
void DisplayAge(const Human& person){
cout << person.age << endl;
}
  1. 将外部类指定为可信任的朋友。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private:
friend class Utility;
string name;
int age;

// Utility 类的所有方法都能访问Human 类的私有数据成员和方法。
class Utility{
public:
static void DisplayAge(const Human& person){
cout << person.age << endl;
}
};

Utility::DisplayAge(firstMan);
1.4 共用体:一种特殊的数据存储机制
  1. 声明共用体:使用关键字union,再在这个关键字后面指定共用体名称,然后在大括号内指定其数据成员。
1
2
3
4
5
6
7
8
9
10
union UnionName{
Type1 member1;
Type2 menber2;
···
TypeN memberN;
};

// 实例化并使用共用体
UnionName unionObject;
unionObject.member2 = value; // choose member2 as the active member
  • 共用体与类一样,可包含多个数据成员,但不同的是只能使用其中的一个。
  • 与结构类似,共用体的成员默认也是公有的,但不同的是,共用体不能继承。
  • 另外,将sizeof()用于共用体时,结果总是为共用体最大成员的长度,即便该成员并不处于活动状态。

2. 构造函数

构造函数是一种特殊的函数(方法),在根据类创建对象时被调用。与函数一样,构造函数也可以重载。

2.1 声明和实现构造函数
  1. 构造函数是一种特殊的函数,它与类同名且不返回任何值。
1
2
3
4
calss Human{
public:
Human();
}
  1. 构造函数可在类声明中实现,也可在类声明外实现。
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(){ // ::被称为作用域解析运算符

}
  1. 使用构造函数:构造函数总是在创建对象时被调用,这让构造函数成为将类成员变量(int、指针等)初始化为选定值的理想场所。
  2. 可在不提供参数的情况下调用的构造函数被称为默认构造函数。默认构造函数是可选的。这种构造函数会创建成员属性,但不会将POD 类型(如int)的属性初始化为非零值。
  3. 重载构造函数
1
2
3
4
5
6
7
8
9
10
11
12
class Human
{
public:
Human()
{
// default constructor code here
}
Human(string humansName)
{
// overloaded constructor code here
}
};
  1. 没有默认构造函数的类:类没有默认构造函数,创建类对象时必须提供参数。
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. 带默认值的构造函数参数:默认构造函数是调用时可不提供参数的构造函数,而并不一定是不接受任何参数的构造函数。
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. 包含初始化列表的构造函数
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);
  1. 也可使用关键字constexpr 将构造函数定义为常量表达式。
1
2
3
4
5
6
7
8
class Sample
{
const char* someString;
public:
constexpr Sample(const char* input):someString(input)
{ // constructor code
}
};
2.2 复制构造函数
  1. 类包含原始指针成员(char *等)时,务必编写复制构造函数和复制赋值运算符。
  2. 编写复制构造函数时,务必将接受源对象的参数声明为const 引用。
  3. 使用复制构造函数确保深复制(复制构造函数接受一个以引用方式传入的当前类的对象作为参数,分配了新的内存地址)。
  4. 浅复制并复制指向的内存单元,导致两个对象指向同一个内存单元,调用函数返回后,使用析构销毁变量,导致main函数中的对象指向无效内存。

3. 析构函数

析构函数在对象销毁时自动被调用。

3.1 声明和实现析构函数
  1. 析构函数看起来像一个与类同名的函数,但前面有一个腭化符号(~)。
1
2
3
class Human{
~Human();
}
  1. 这个析构函数可在类声明中实现,也可在类声明外实现。
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(){

}
  1. 使用析构函数

每当对象不再在作用域内或通过delete 被删除进而被销毁时,都将调用析构函数。这使得析构函数成为重置变量以及释放动态分配的内存和其他资源的理想场所。

  1. 析构函数不能重载,每个类都只能有一个析构函数。如果您忘记了实现析构函数,编译器将创建一个伪(dummy)析构函数并调用它。伪析构函数为空,即不释放动态分配的内存。
3.2 构造函数和析构函数的其他用途
  1. 不允许复制的类:要禁止类对象被复制, 可声明一个私有的复制构造函数。
  2. 只能有一个实例的单例类:使用单例的概念,它使用私有构造函数、私有赋值运算符和静态实例成员。
  • 将关键字static 用于类的数据成员时,该数据成员将在所有实例之间共享。
  • 将static 用于函数中声明的局部变量时,该变量的值将在两次调用之间保持不变。
  • 将static 用于成员函数(方法)时,该方法将在所有成员之间共享。
  1. 禁止在栈中实例化的类:栈空间通常有限。如果您要编写一个数据库类,其内部结构包含数TB 数据,可能应该禁止在栈上实例化它,而只允许在自由存储区中创建其实例。为此,关键在于将析构函数声明为私有的。
1
2
3
4
5
6
7
8
9
10
11
class MonsterDB{
private:
~MonsterDB(); // private destructor
}

// 可以禁止下面的实例创建
int main(){
MonsterDB myDatabase; // compile error

return 0;
}

4. 封装

  1. 封装的意义一
  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制
1
2
3
4
// 语法
class 类名{
访问权限: 属性 / 行为
};
  1. 封装的意义二:类在设计时,可以把属性和行为放在不同的权限下,加以控制。访问权限有三种(访问修饰符):
  • public 公共权限
  • protected 保护权限
  • private 私有权限

5. 继承

5.1 继承和派生

image-20240402155759800

image-20240402162226768
  1. 派生语法
1
2
3
4
5
6
7
8
9
class Base{
// ... base class members
};

class Derived: access-specifier Base{
// ... derived class members
};

//其中access-specifier 可以是public(这是最常见的,表示派生类是一个基类)、private 或 protected(表示派生类有一个基类)。
1
2
3
4
5
6
7
8
// 由Fish派生出Carp
class Fish { // base class
// ... Fish's members
};

class Carp:public Fish{ // derived class
// ... Carp's members
};
  1. 访问限定符protected:需要让基类的某些属性能在派生类中访问,但不能在继承层次结构外部访问,可使用关键字protected。

与public和private一样,protected也是一个访问限定符。将属性声明为protected时,相当于允许派生类和友元类访问它,但禁止在继承层次结构外部包括main访问它。

  1. 基类初始化–向基类传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
class Base{
public:
Base(int someNumber){ // 重载构造函数
// Use someNumber
}
};

class Derived:public Base{
public:
Derived(): Base(25){ // 实例化Base 参数
// derived class constructor code
}
};
  1. 派生类中覆盖基类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base
{
public:
void DoSomething()
{
// implementation code… Does something
}
};
class Derived:public Base
{
public:
void DoSomething()
{
// implementation code… Does something else
}
};
  1. 构造顺序
  • 基类对象在派生类对象之前被实例化,先构造派生类对象的基类部分(成员属性)。
  • 实例化基类部分和派生类部分时,先实例化成员属性,再调用构造函数。
  • 顺序:初始化基类成员属性(调用属性的构造函数),再调用基类的构造函数;初始化派生类成员属性,再调用派生类的构造函数。
  1. 析构顺序
  • 与构造顺序相反。
  • 顺序:调用派生类的析构函数,再调用成员属性的析构函数;再调用基类的析构函数,最后调用基类成员属性的析构函数。
  1. 私有继承:指定派生类的基类时使用关键字private,私有继承意味着在派生类的实例中,基类的所有公有成员和方法都是私有的—-不能从外部访问。
1
2
3
4
5
6
7
8
9
class Base
{
// ... base class members and methods
};
class Derived: private Base // private inheritance
{
// ... derived class members and methods
};
// 只能被Derived类使用,无法通过Derived实例来使用。
  1. 保护继承:保护继承不同于公有继承之处在于,声明派生类继承基类时使用关键字protected。

保护继承与私有继承的类似之处如下:

  • 它也表示has-a 关系;
  • 它也让派生类能够访问基类的所有公有和保护成员;
  • 在继承层次结构外面,也不能通过派生类实例访问基类的公有成员。

不同之处:

  • 在保护继承层次结构中,子类的子类(即Derived2)能够访问Base 类的公有和保护成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base
{
// ... base class members and methods
};
class Derived: protected Base // protected inheritance
{
// ... derived class members and methods
};

class Derived2: protected Derived
{
// can access public & protected members of Base
};
// 如果Derived 和Base 之间的继承关系是私有的,就不能这样做。
  1. 多继承
1
2
3
4
class Derived: access-specifier Base1, access-specifier Base2
{
// class members
};
  1. 使用final禁止继承:被声明为final 的类不能用作基类

6. 多态

多态(Polymorphism)是面向对象语言的一种特征,让您能够以类似的方式处理不同类似的对象

  1. 使用虚函数实现多态行为:通过使用关键字virtual,可确保编译器调用覆盖版本。将派生类对象视为基类对象。
1
2
3
4
5
6
7
8
class Base
{
virtual ReturnType FunctionName (Parameter List);
};
class Derived
{
ReturnType FunctionName (Parameter List);
};
  1. 对于使用new 在自由存储区中实例化的派生类对象,如果将其赋给基类指针,并通过该指针调用delete,将不会调用派生类的析构函数。这可能导致资源未释放、内存泄露等问题。要避免这种问题,可将析构函数声明为虚函数。
1
2
3
4
5
6
// 可避免将delete 用于Base 指针时,派生类实例未被妥善销毁的情况发生。
class Base
{
public:
virtual ~Base() {}; // virtual destructor
};
  1. 虚函数工作原理—-虚函数表:编译器为实现了虚函数的基类和覆盖了虚函数的派生类分别创建一个虚函数表(VFT)。
  2. 不能实例化的基类被称为抽象基类,这样的基类只有一个用途,那就是从它派生出其他类。在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; // pure virtual method
};

// 派生类必需实现方法DoSomthing()
class Derived: public AbstractBase
{
public:
void DoSomething() // pure virtual fn. must be implemented
{
cout << "Implemented virtual function" << endl;
}
};

// 编译器不允许创建抽象基类的实例
// AbstractBase ab; // 错误,不能创建抽象类的对象
  1. 在继承层次结构中,继承多个从同一个类派生而来的基类时,如果这些基类没有采用虚继承,将导致二义性。这种二义性被称为菱形问题(Diamond Problem)。
  2. 使用限定符 override 来核实被覆盖的函数在基类中是否被声明为虚函数。
1
2
3
4
void Swim() const override // Error: no virtual fn with this sig in Fish
{
cout << "Tuna swims!" << endl;
}
  1. 使用 final 来禁止覆盖函数。
1
2
3
4
5
// override Fish::Swim and make this final
void Swim() override final
{
cout << "Tuna swims!" << endl;
}
  1. 不能将复制构造函数声明为虚函数
  • 不可能实现虚复制构造函数,因为在基类方法声明中使用关键字virtual 时,表示它将被派生类的实现覆盖,这种多态行为是在运行阶段实现的。而构造函数只能创建固定类型的对象,不具备多态性,因此C++不允许使用虚复制构造函数。

  • 但存在一种不错的解决方案,就是定义自己的克隆函数来实现上述目的

7. this指针

  1. 关键字this 包含当前对象的地址,其值为&object。当您在类成员方法中调用其他成员方法时,编译器将隐式地传递this 指针—函数调用中不可见的参数。
  • 调用静态方法时,不会隐式地传递this 指针,因为静态函数不与类实例相关联,而由所有实例共享。
  • 要在静态函数中使用实例变量,应显式地声明一个形参,并将实参设置为this 指针。
  1. sizeof()用于类及其对象时,结果相同,因为类占用的字节数在编译阶段就已经确定。
1
2
3
4
5
6
private:
int age; // 4 字节
bool gender; // 1 字节
MyString name; // 8 字节

// sizeof() = 16 -> 4 + 4(1+3) + 8 = 16

8. 运算符类型与运算符重载

8.1 单目运算符
  1. 单目运算符只对一个操作数进行操作。实现为全局函数或静态成员函数的单目运算符的典型定义如下:
1
2
3
return_type operator operator_type(parameter_type){
// 实现
}
  1. 作为类成员(非静态函数)的单目运算符没有参数,因为它们使用的唯一参数是当前类实例(*this),如下所示:
1
2
3
return_type operator operator_type(){
// 实现
}
  1. 单目运算符的类型:可以重载(或重新定义)的单目运算符:
运算符名称运算符名称
++递增&取址
—-递减~求反
*解除引用+
->成员选择-
!逻辑非转换运算符转换为其他类型
  1. 递增&递减
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
// 1. 单目前缀递增运算符(++)
Date& operator ++ ()
{
// operator implementation code
++VARIABLE
return *this;
}

// 2. 后缀递增运算符(++)的返回类型不同,且有一个输入参数(但并非总是使用它)
Date operator ++ (int)
{
// Store a copy of the current state of the object, before incrementing day
Date copy (*this);
// increment implementation code
VARIABLE++
// Return state before increment (because, postfix)
return copy;
}

// 3. 单目前缀递减运算符(--)
Date& operator -- ()
{
// operator implementation code
--VARIABLE
return *this;
}

// 4. 后缀递减运算符(--)的返回类型不同,且有一个输入参数(但并非总是使用它)
Date operator -- (int)
{
// Store a copy of the current state of the object, before incrementing day
Date copy (*this);
// increment implementation code
VARIABLE-
// Return state before increment (because, postfix)
return copy;
}
8.2 转换运算符
  1. 当类不支持当前的运算符是,将类的对象的内容转换成cout能够接受的类型。例如const char*,只需添加一个返回const char*的运算符。
1
2
3
operator const char*(){
// operator implementation that returns a char*
}
8.3 解除引用运算符(*)和成员选择运算符(->)
  1. 解除引用运算符(*)和成员选择运算符(->)在智能指针类编程中应用最广。智能指针是封装常规指针的类,旨在通过管理所有权和复制问题简化内存管理。在有些情况下,智能指针甚至能够提高应用程序的性能。
8.3 双目运算符
  1. 对两个操作数进行操作的运算符称为双目运算符。以全局函数或静态成员函数的方式实现的双目运算符的定义如下:
1
return_type operator_type (parameter1, parameter2);
  1. 以类成员的方式实现的双目运算符只接受一个参数,其原因是第二个参数通常是从类属性获得的。以类成员的方式实现的双目运算符的定义如下:
1
return_type operator_type (parameter);
  1. 双目运算符的类型
运算符名称运算符名称
,逗号<小于
!=不等于<<左移
%求模<<=左移并赋值
%=求模并赋值<=小于或等于
&按位与=赋值、复制赋值和移动赋值
&&逻辑与==等于
&=按位与并赋值>大于
*>=大于或等于
*=乘并赋值>>右移
+>>=右移并赋值
+=加并赋值^异或
-^=异或并赋值
-=减并赋值|按位或
->*指向成员的指针|=按位或并赋值
/||逻辑或
/=除并赋值[]下标运算符
  1. 双目加法与双目减法运算:与递增/递减运算符类似,如果类实现了双目加法和双目减法运算符,便可将其对象加上或减去指定类型的值。

  2. 重载等于运算符(==)和不等运算符(!=)

  由于还没有定义等于运算符(==),编译器将对这两个对象进行二进制比较,并仅当它们完全相同时才返回true。对于包含简单数据类型的类,这种二进制比较是可行的。然而,如果类有一个非静态字符串成员,它包含字符串值(char *),则比较结果可能不符合预期。在这种情况下,对成员属性进行二进制比较时,实际上将比较字符串指针,而字符串指针并不相等(即使指向的内容相同),因此总是返回false。为了解决这种问题,可定义比较运算符。

1
2
3
4
5
6
7
8
9
10
11
// 等式
bool operator== (const ClassType& compareTo)
{
// comparison code here, return true if equal else false
}

// 不等式
bool operator!= (const ClassType& compareTo)
{
// comparison code here, return true if inequal else false
}
  1. 下标运算符:下标运算符能够像访问数组那样访问类,其典型语法如下
1
return_type& operator [] (subscript_type& subscript);

​ 编写封装了动态数组的类(如封装了char* buffer 的MyString)时,通过实现下标运算符,可轻松地随机访问缓冲区中的各个字符:

1
2
3
4
5
6
7
8
class MyString{
// ... other class members
public:
/*const*/ char& operator [] (int index) /*const*/
{
// return the char at position index in buffer
}
};
8.4 函数运算符 operator()
  1. operator()让对象像函数,被称为函数运算符。函数运算符用于标准模板库(STL)中,通常是STL算法中,其用途包括决策。根据使用的操作数数量,这样的函数对象通常称为单目谓词或双目谓词。
8.5 移动构造函数和移动赋值运算符 **
  1. 移动构造函数和移动赋值运算符乃性能优化功能,属于C++11 标准的一部分,旨在避免复制不必要的临时值(当前语句执行完毕后就不再存在的右值)。对于那些管理动态分配资源的类,如动态数组类或字符串类,这很有用。
  2. 声明移动构造函数和移动赋值运算符:
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){ // Move constructor, note &&
ptrResource = moveSource.ptrResource; // take ownership, start move
moveSource.ptrResource = NULL;
}

Sample& operator= (Sample&& moveSource){//move assignment operator, note &&
if(this != &moveSource){
delete [] ptrResource; // free own resource
ptrResource = moveSource.ptrResource; // take ownership, start move
moveSource.ptrResource = NULL; // free move source of ownership
}
}
Sample(); // default constructor
Sample(const Sample& copySource); // copy constructor
Sample& operator= (const Sample& copySource); // copy assignment
};

​ 移动构造函数和移动赋值运算符的不同之处在于,输入参数的类型为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

// 使用operate "" 来定义
ReturnType operator "" YourLiteral(ValueType value)
{
// conversion code here
}

#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. 使用语法
1
destination_type result = cast_operator<destination_type> (object_to_cast);
  1. static_cast

使用static_cast 可将指针向上转换为基类类型,也可向下转换为派生类型。

1
2
Base* objBase = new Derived();
Derived* objDer = static_cast<Derived*>(objBase);
  1. dynamic_cast

与静态类型转换相反,动态类型转换在运行阶段(即应用程序运行时)执行类型转换。可检查dynamic_cast 操作的结果,以判断类型转换是否成功。

1
2
3
4
5
Base* objBase = new Derived();
Derived* objDer = dynamic_cast<Derived*>(objBase);

if(objDer)
objDer->CallDerivedFunction();
  1. reinterpret_cast

将一种对象类型转换为另一种,不管它们是否相关。这种类型转换实际上是强制编译器接受static_cast 通常不允许的类型转换,通常用于低级程序。

1
2
Base* objBase = new Base();
Unrelated* notRelated = reinterpret_cast<Unrelated*>(objBase);
  1. const_cast

const_cast的大部分使用主要是将常量指针转换为常指针。常量指针指向的空间的内容不允许被修改,但是使用const_cast进行强制转换就可以修改。


C++语法学习(二)
http://seulqxq.top/posts/54407/
作者
SeulQxQ
发布于
2024年4月14日
许可协议