C++中的类

C++的目标之一是让使用类对象就像使用标准类型一样。 ---- C++ Primer Plus

一、最重要的OOP特性:

  • 抽象;
  • 封装和数据隐藏;
  • 多态;
  • 继承;
  • 代码的可重用性;

​ C++通过类使得实现抽象,数据隐藏和封装等OOP特性很容易。类的公有部分的内容构成了设计的抽象部分---公有接口。将数据封装到私有部分中可以保护数据的完整性,这称为数据隐藏。

二、构造函数

1、为什么使用构造函数?

int year = 2001;    // valid initialization
struct thing{
     char *pn;
      int n;
};
thing a = {"a",1};  // valid initialization
Stock s = {"a",1};  // Stock is a class, compile error

​ 无法这样做的原因是类的数据是私有的,程序不能直接访问,解决这个问题的方法有:

  • 将数据声明为公有,但这样违反了类的一个主要初衷:数据隐藏;
  • 设计一个普通公有函数func()给数据赋值,但这种方式需要假设用户在调用任何其他方法前调用 func(),无法强加这个假设;
  • 提供一个特殊公有函数,在创建对象时,自动调用,完成初始化工作;这个特殊的函数就是构造函数(constructor)。

2、构造函数的功能

​ 在创建对象时,完成初始化工作,这个初始化工作包括以下:

  • 为对象申请地址空间;
  • 给变量赋值;

3、特殊的构造函数

3.1、默认构造函数

3.1.1、作用

​ 在未提供显示初始值时,用来构造对象,像Stock s;

3.1.2、三种类型
  • c++编译器自动生成的,只创建对象,但不初始化其成员。函数定义如下:

Stock::Stock(){};

  • 通过函数重载,我们可以定义另外一种默认构造函数:
    Stock::Stock(){
        company = ...;
        shares = ...;
    }

​ 这样的默认构造函数可以初始化成员。

  • 第三种默认构造函数,他的参数全是默认参数:
    Stock::Stock(const string & name = "nobody",int n=0){
        company = ...;
        shares = ...;
    }
  • 三种默认构造函数不能同时出现,否则会有二义性错误。

3.2、类型转换函数

3.2.1、类型转换的原因

​ 内置类型可以进行自动类型转换(隐式转换)和强制类型转换(显式转换),因此,类也应该可以进行类型的转换,当然,转换要有意义

3.2.2、转换构造函数(别的类型转换成类)
  • 定义:只有接受一个参数(第二、三个参数是默认参数也行)的构造函数才能作为转换构造函数。

  • 使用:

    Stock(int n);   // constructor
    Stock s;    // create a Stock object
    s = 5;  // use Stock(int n) to convert 5 to Stock;
    

​ 上述的过程:程序使用 Stock(int) 来创建一个临时对象,并将5作为初始化值。随后,采用逐成员赋值的方式将盖临时对象的内容复制到s中。这个过程就是 隐式转换

explicit(显式)关键字可以关闭构造函数的这个功能,只允许显式强制转换,最好关了。

3.2.3、转换函数(类转换成别的类型)
  • 转换函数是用户定义的强制类型转换,可以像使用内置类型的强制类型转换一样来使用。

  • 函数声明:operator typename();

  • 使用:

    Stock s(5);
    int m = s; 
    int n = (int)s; 
    int k = int(s); // 这三种使用方法都可以
    
  • 注意:

    1. 转换函数必须是类方法; // 需要访问成员
    2. 转换函数不能指定返回类型; // typename指出了返回类型
    3. 转换函数不能有参数; // 类方法指明它通过类对象来调用,从而告知函数要转换的参数

3.3、复制构造函数

3.3.1、原因

​ 为了提高内存的利用率,通常,最好是在程序运行时(而不是编译时)确定诸如使用多少内存的问题。在C++中,使用 newdelete 来申请和释放 中的内存,将地址存在成员变量中,像下面这样:char * str = new char[len+1]; len+1char放在堆中,首地址放在str中。

​ 浅复制:逐个复制非静态成员,复制的是成员的值。由上述的例子可以看出,当成员变量是一个指向堆中内存的指针时,浅复制会导致一个问题:两个指针指向一块内存区。当 delete时同一块内存会释放两次,指针悬挂。

​ 浅复制问题的根源是只复制了指针,未复制指针指向的内容。因此,我们需要一个可以复制指向内容的复制函数。

​ C++提供默认的复制构造函数,它可以实现浅复制。为了实现深度复制,我们需要显式定义复制构造函数。

3.3.2、声明

​ 通常如下:Class_name(const Class_name &);

3.3.3、使用

​ 什么时候调用复制构造函数:用对象初始化,按值传递参数和返回对象时。因为按值传递参数会调用复制构造函数,所以应该使用引用来传递对象,这样可以节省构造函数的时间和新对象的空间。

​ 当类中有静态成员时,要注意一下,默认的复制构造函数不说明其行为,因此它不指出创建过程,而析构函数不管对象是如何创建的,在对象过期时都会被调用。

3.4、赋值运算符(类似于复制构造函数)

​ 内置数据类型可以赋值,ANSI C允许结构体赋值,C++允许类对象赋值。在C++中,是通过为类重载赋值运算符实现的。C++提供默认的赋值运算符,实现类似于浅复制的功能,为实现深度赋值的功能,需自行重载赋值运算符。

3.4.1、函数原型:

Class_name & Class_name::operator=(const Class_name &);

3.4.2、何时使用赋值运算符

​ 将已有的对象赋给另一个对象时,将使用重载的赋值运算符。

3.4.3、赋值运算符和复制构造函数

​ 初始化时总是会调用复制构造函数,而使用=运算符时也允许调用赋值运算符,像下面这样:

StringBad headline1("a");
StringBad headline2;
headline2 = headline1;  // assignment operator invoked
StringBad headline3 = headline1;  // use copy constructor,possibly assignment too
3.4.4、使用时注意的问题
  • 由于目标对象可能引用了以前分配的数据,所以函数应该使用delete来释放这些数据。
  • 函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存的操作可能会删除对象的内容。
  • 函数返回一个指向调用对象的引用。
  • 赋值运算符只能由类成员函数重载。

三、普通成员函数

1、函数

1.1、函数原型

  • eg:void func(int);
  • 函数原型描述了编译器的接口,他将函数返回值的类型和参数的类型数量告诉了编译器。
  • 避免使用函数原型的唯一方法是在首次使用函数之前定义它,但这并不总是可行。
  • 函数原型是一条语句,以分号结束;函数原型不要求提供变量名,有类型列表就足够了。

1.2、函数参数

1.2.1、按值传递
  • 将数值参数传递给函数,而后者将赋给一个新的变量。
  • 传递的是实参的副本,更改副本不会对实参本身造成影响。
  • 传递时要创建副本,对于复杂数据结构比较耗费时间和空间。
1.2.2、引用参数
  • 引用是已定义的变量的别名,引用变量的主要用途是用作函数的形参以传递复杂数据结构(结构体,类)。通过将引用变量用作参数,函数将使用原始数据而不是副本。
  • eg:int n; int &m = n; //m为n的别名,二者指向同一块内存单元 ;引用接近 const指针,必须在创建时初始化,一旦与某个变量关联起来,就一直效忠于它。
  • 当实参和引用参数不匹配时,C++将生成临时变量,引用为临时变量的别名。不匹配的情况有两种:类型正确,但不为左值;类型不正确,但可转换成正确的类型。这种机制存在问题,当接受引用参数的函数想修改参数的值时,这种机制会阻止这种意图的实现。因此,对于普通的引用参数应该消除这种机制。但参数为const引用时则不会有这个问题。
  • const引用作参数:让函数使用传递的参数但不修改,结合临时变量,可以让函数在可处理的参数种类方面更通用。
  • 左值和右值:
    1. 左值:可通过地址访问的值。
    2. 右值:不能通过地址访问的值。
    3. 赋值表达式的左边是可修改的左值,即左边的子表达式必须要标识一个可修改的块。
    4. 常规函数(非引用返回)的返回值是右值,因为返回值位于临时内存单元中,运行到下一条语句时,他们可能就不存在了。
1.2.3、指针参数
1.2.4、默认参数
  • 当函数调用中省略了实参时自动使用的一个值。
  • 必须通过函数原型来设置默认参数。
  • 对于带参数的函数,必须从右向左添加默认值,实参从左到右的顺序依次被赋给相应的形参。

1.3、函数返回值

  • C++对返回值的类型有一定的限制:不能是数组,但可以是其他任何类型,包括:整数,浮点数,指针,甚至是结构和对象。
  • 返回值可以是常量,变量,也可以是表达式。
  • 引用作返回值效率更高,但返回值可充当左值,这有时会出错,const引用作返回值可以消除这个错误。

2、函数指针

2.1、什么是函数指针

​ 存储函数地址的变量,函数地址是存储其机器语言代码的内存的开始地址。

2.2、为什么要用函数指针

​ 允许在不同的时间传递不同函数的地址。

2.3、怎么用函数指针

  1. 获取函数地址(使用函数名)

  2. 声明一个函数指针

    ​ 须指明指针指向的函数类型,包括返回值,参数列表。

    double pan(int);    //prototype
    double (*pf)(int);  //pf points to a function
    
  3. 使用函数指针来调用函数

pf = pan; //pf points to pan
double x = pan(1); == double x = (*pf)(1); //pf是指向pan()的指针,(*pf)就是函数。
```

3、重载

  • 为什么要重载?

    多态性的要求,即函数基本上执行相同的任务,但使用不同形式的数据,这样做隐藏了内部机理,强调了实质,是OOP的另一个目标。 ------ C++ Primer Plus

3.1、函数重载

3.1.1、什么是函数重载
两个函数的函数名相同,但特征标(参数的数目,类型,顺序)不同。
3.1.2、怎么样进行函数重载
与普通的函数声明,使用一样。
3.1.3、注意
  • 没有匹配的原型时,并不会自动停止使用其中的某个函数,C++将尝试使用标准类型转换强制进行匹配。若有多个原型可匹配,会报错。
  • 编译器在检查函数特征标时,将类型引用和类型本身视为同一特征标。
  • 匹配函数时,并不区分const和非const变量。

3.2、运算符重载

  • 根据OOP的设计理念,C++允许将运算符重载到用户定义的类型(类)。
  • 运算符函数的使用会访问类的成员变量,因此重载方式有:
    1. 由类的成员函数来重载;
    2. 由类的友元函数来重载;
3.2.1、类的成员函数
  • 声明:operatorop(argumentList);
  • 调用:1. objectName.operatorop(argument);
    2. objectName op argument;
  • 注意:
    1. 重载的运算符必须有一个操作数是用户定义的类型;
    2. 使用运算符不能违反原来的句法规则;
    3. 有些运算符比较特殊;
    4. 运算符使用时左侧的对象是调用的对象,右侧的对象是参数;
    5. 重载时要考虑参数互换位置的情况;
3.2.2、友元函数
  • 为何需要友元函数来重载运算符?

      当运算符有两个操作数时,由类的成员函数来重载会限制运算符的使用。例如类和整数的乘法有两种表现形式:类 * 整数 和 整数 * 类,前者在使用运算符时没问题,但后者无法进行,这时就需要用友元函数来进行重载。
    
  • 函数声明:在函数原型前加 friend关键字。

  • 使用:友元函数不是类的成员函数,但访问权限和类的成员函数相同。

四、数据成员

1、基本数据类型(整型,浮点型)

1.1、整型

  • 有符号:shortintlonglong long
  • 无符号:在前四种前加 unsigned
  • 字符型:char
  • 布尔型:bool

1.2、浮点型

  • floatdoublelong double

1.3、类型转换

1.3.1、原因
计算机处理不同算数类型的情况不同,为避免潜在的混乱。
1.3.2、自动类型转换
  • 在以下情况中,C++会进行自动类型转换:
    1. 将一种算数类型的值赋给另一种算数类型的变量时(包含初始化);
    2. 表达式中包含不同的数据类型时;
    3. 将参数传递给函数时;
1.3.3、强制类型转换
  • 强制类型转换不会修改变量本身,而是创建一个新的,指定类型的值,可以在表达式中使用这个值。
  • 使用强制类型转换的原因一般是使一种格式的数据满足不同的期望。

1.4、const限定符

  • 可以完成#define预编译指令的功能(在预编译阶段将对应的符号名替换成常量)
  • 格式:const type name = value; #define name value
  • 相较于 #define const 的优点:
    1. 可以明确指定类型;
    2. 可以使用C++的作用域规则将定义限制在特定的函数或文件中;
    3. const可以用于更复杂的数据类型

1.5、初始化

  • 初始化将声明和赋值合并到一起。
  • 方式:
    1. int n = 10;
    2. int n = {10};
  • 第二种方式称为初始化列表,常用于数组和结构,当{}中空时,初始化为0;不允许缩窄,可以更好地防范类型转换错误。

2、数组(array)

  • 一种数据格式,能存储多个同类型的数据。

  • 声明:typename arrayName[arraySize]; arraySize的值在编译阶段必须已知,注:变量的值是在程序运行时才设置的。

  • 初始化:int a[2] = {0,1};

    1. 如果没有初始化数组,其元素的值是不确定的;
    2. 只有定义数组时才能使用初始化,此后就不能用了,也不能将一个数组赋值给另一个数组,可以用下标分别给数组中的元素赋值。
  • 注意:数组不能作参数,但可以使用指针来传递数组。

    int data[3][4] = {{1,1,1,1},{2,2,2,2},{3,3,3,3}};
    // 将data数组传入函数
    int total = sum(data,3);
    // sum()的函数声明有两种方式
    int sum(int (*array)[4],int size);
    int sum(int array[][4],int size);
    

3、共用体(union

  • 句法和结构相似,但含义不同。

  • 声明:

    union u{
      int a;
      float b;
      double c;
    };
    
  • 联合是一种数据格式,能够存储不同的数据类型,但只能同时存储其中的一种。

  • 联合的用途主要是节省空间,常用于操作系统数据结构或硬件数据结构。

4、枚举(enum

4.1、普通枚举

  • 枚举提供了一种创建符号常量的方式,可以用来替代const

  •   enum e{
          a,
          b = 9
      };
    
  • e为枚举,a为枚举量,是符号常量,是整型,可提升为intint也可强制转换成枚举量,如果值合适的话。

  • 第一个枚举量默认为0,依次类推。可以显式的指定枚举量的值,显式设置某个枚举量的值后,其后的没被初始化的枚举量的比前面的大1。可以设置多个值相同的枚举量。

  • 枚举只定义了赋值运算符(=),所以枚举更常用来定义相关的符号常量。

4.2、作用域内为类的常量

  • 类声明中可能会使用常量,怎么办?
    1. const int n = 2; int a[n]; 符号常量,不行,因为声明类只是描述了对象的形式,没有创建对象,没有存储空间,成员初始化机制可以解决这个问题。
    2. enum{n=2}; int a[n]; 枚举量,可行。枚举并不会创建数据成员,编译器会替换。
    3. static const int n = 2; int a[n]; 静态常量,不是存储在对象中,可行。

4.3、作用域内枚举

  • 两个枚举定义中的枚举量可能会冲突,怎么办? --- 新枚举,枚举量的作用域为类。
  • 声明:enum class/struct enumName{....};
  • 使用:enumName::....;
  • 作用域内枚举不能隐式转换成整型。

5、结构体(struct

  • 一种数据格式,同一结构体可以存储多种类型的数据,是OOP类的基石。

  • 声明:

    struct s{
        int a;
        char c;
        bool b;
    };
    
  • 初始化:初始化列表

  • 结构体成员赋值:可以使用赋值运算符(=)将结构赋给另一个同类型的结构,即便成员有数组。

6、指针(C++中内存管理编程理念的核心)

​ 面向对象编程和过程性编程的区别在于,OOP强调的是在运行阶段(而不是编译阶段)进行决策。C++采用的方法是,使用new请求正确数量的内存以及使用指针来跟踪新分配的内存的位置。 ------ C++ Primer Plus

6.1、什么是指针

​ 指针是一种特殊类型的变量,用于存储值的地址。

6.2、为什么要用指针

  • 计算机程序在存储数据时必须追踪的三种基本属性:
    1. 信息存储在何处
    2. 存储的值为多少
    3. 存储的信息的类型
  • 两种方式:
    1. 定义一个常规变量,int n;,声明语句指出了值的类型和符号名,让程序为值分配内存,并在内部跟踪该内存单元。
    2. 定义一个指针变量,int* n;n是一个指向int的指针。
  • 比较:第一种方式以值为中心,第二中方式以地址为中心。

6.3、如何使用指针

  • 声明和初始化:

    int* n; //声明
    int m = 10;
    int* n = &m;    //初始化
    int* p = new int;   //与new联用,运行阶段在堆中分配地址
    
  • 使用,对指针解除引用:

    int c = *n; //c = 10;
    

6.4、指针和const

  • 两种方式:

    1. 让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值。

      int age = 0; const int* p = &age;

    2. 将指针本身声明为常量,这样可以防止改变指针指向的位置。

      int age = 0; int* const p = &age;

  • 应尽量将指针参数声明为const

    1. 可以避免由于无意间修改数据导致的错误;
    2. 使用const能使函数可以接受const和非const参数,使用非const只能接受非const参数,有例外的情形。

6.5、注意

  • 在C++中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存。所以,一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的,适当的地址。
  • 指针与new运算符联用时,要注意正确使用delete来释放申请的内存。

7、字符串

7.1、指针和数组

  • C++中,指针和数组基本等价:

    int* p = new int[10];   // new 返回的是第一个元素的地址
    p[0] = 1;
    p[9] = 10;
    
  • 指针和数组名的区别:

    1. 一般情况下,C++将数组名解释为地址,但有例外。

    2. 数组名是常量,指针是变量;

    3. 对数组应用sizeof运算符得到的是数组的长度,对指针应用sizeof得到的是指针的长度,在这种情况下,C++不会将数组名解释为地址。

    4. 对数组取地址,数组名也不会被解释为地址。

      short tell[10];
      cout << tell << endl;   // displays &tell[0]
      cout << &tell < endl;  // displays address of the whole array
      

      &tell是这样一个指针,它指向一块包含10个short元素的内存空间,short (*)[10]short (*p)[10] = &tell;

      tell 是数组第一个元素的地址,short *short* p = tell;

7.2、字符串

  • cout和多数C++表达式中,char数组名,char指针以及用引号括起来的字符串常量都被解释为字符串第一个字符的地址。

原文链接: https://www.cnblogs.com/xxiaobai/p/12659456.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    C++中的类

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/340637

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月2日 上午12:42
下一篇 2023年3月2日 上午12:42

相关推荐