C++初阶---类和对象

目录

1. 类的引入

2. 类的定义

4. 类的访问限定符及封装

4.1 访问限定符

4.2 封装

5. 类的作用域

6.类的实例化

7.类对象模型

8. this指针

8.1 this指针的引出

8.2 this指针的特性

8.3 C语言和C++实现栈的对比

9.类的六个默认成员函数

10,构造函数

10.1 概念

10.2 特性

10.2 构造函数整体赋值

10.3 初始化列表

10.4 explicit关键字

11. 析构函数

11.1 概念

11.2 特性

12.拷贝构造函数

12.1 概念

12.2 特征

13.赋值运算符重载

13.1 运算符重载

13.2 赋值运算符重载

13.3 前置++和后置++重载

14. const 成员和static成员

14.1 const成员

14.2 static 成员

15. 友元函数和友元类

15.1 友元函数

15.2 友元类

16 内部类


1. 类的引入

        C语言结构体中只能定义变量,而在C++中,结构体不仅仅可以定义变量,还可以定义函数。但是相对于对结构体的定义在C语言中使用struct,而在C++中更喜欢使用class代替,而使用了c++方式来定义结构体,就会发现一些简单的结构,如栈,队列等,在实现后会使用起来会方便很多。如栈的实现:

// C++实现
typedef int DataType;
struct Stack {
    void Init(size_t capacity) {
        _array = (DataType*)malloc(sizeof(DataType) * capacity);
        if (nullptr == _array) {
            perror("malloc申请空间失败");
            return;
        }
        _capacity = capacity;
        _size = 0;
    }
    void Push(const DataType& data) {
        // 扩容
        _array[_size] = data;
        ++_size;
    }
    DataType Top() { return _array[_size - 1]; }
    void Destroy() {
        if (_array) {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }
    DataType* _array;
    size_t _capacity;
    size_t _size;
};
int main() {
    Stack s;
    s.Init(10);
    s.Push(1);
    s.Push(2);
    s.Push(3);
    cout << s.Top() << endl;
    s.Destroy();
    return 0;
}

2. 类的定义

class classname{

        类体;由成员函数和成员变量组成

};

        其中class为定义类的关键字,classname为类的名称,{ }中为类的主体,注意类定义结束时后边的分号不能省略。

        类体中的内容称为类的成员,类中的变量称为成员变量,类中的方法和函数称为类的方法或者成员函数。

 类的两种定义方法:

        1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。

        2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::

4. 类的访问限定符及封装

4.1 访问限定符

        C++实现封装的方式:用类和对象的属性与方法结合在一起,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

        

【访问限定说明】

        1. public修饰的成员在类外可以直接被访问

        2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)

        3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止

        4. 如果后面没有访问限定符,作用域就到 } 即类结束。

        5. class的默认访问权限为private,struct为public(因为struct要兼容C)

注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。

C++中struct和class的区别是什么?

        C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来 定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类 默认访问权限是private。

4.2 封装

       面向对象的三大特性:封装,继承,多态。

       封装:将数据和操作数据的方法有机的结合,隐藏对象的属性和实现细节,仅对外公开接口和对象进行交互。

        封装本质上是一种管理,让用户更加方便的使用类。在c++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机的结和,通过访问权限来隐藏对象内部的实现细节,控制哪些方法可以在类外部直接被使用。

5. 类的作用域

        类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 : : 作用域操作符来指明成员属于哪一个类域。

classs student{
public:
    void PrintPersonInfo();
private:
    char name[10];
    char gender[3];
    int age;
};
//此处指明PrintPersonInfo是属于student这个作用域
void student::PrintPersonInfo(){


}

6.类的实例化

用类类型来创建面向对象的过程称为类的实例化。

        1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它,

        2. 一个类可以实例化多个对象,实例化出来的对象,占实际的物理空间,存储类成员变量。

  

7.类对象模型

        一个类的大小,实际上就是该类中成员变量之和,要注意内存对齐,注意空类的大小,空类比较特使,编译器给空类一个字节来唯一标识这个类的对象。

结构体内存对齐规则:

        1. 第一个成员在与结构体偏移量为0的地址处。

        2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的对齐数为8

        3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

        4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

常见问题:

1. 结构体怎么对齐? 为什么要进行内存对齐?

        结构体对齐是指编译器在安排结构体中的各个成员时,为了提高访问速度和节省内存,按照特定规则将结构体成员排列在一定的地址上。内存对齐是为了提高访问速度,因为大多数计算机体系结构要求对象的地址符合某些特定的对齐要求,否则会导致性能下降或者错误。

2. 如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?

        可以使用编译器提供的特定指令或者属性来指定结构体的对齐方式,不同的编译器可能有不同的语法。一般来说,可以使用 #pragma pack(n) 或者 __attribute__((aligned(n))) 等指令来实现对齐。对于任意字节对齐,一些编译器可能支持,但并非所有都支持,具体取决于编译器的实现。

3. 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景

        大小端是指在存储多字节数据时,低地址端存放最低有效字节的方式。小端模式是指数据的低字节保存在内存的低地址处,大端模式是指数据的高字节保存在内存的低地址处。你可以通过写一个简单的程序来测试机器的大小端模式,比如定义一个整型变量,然后将其转换为字节序列,然后查看字节序列的顺序即可确定是大端还是小端。

#include<stdio.h>
int main(){
    unsigned int num = 1;
    char *ptr = (char *)&num;
    
    if (*ptr == 1) {
        printf("小端\n");
    } else {
        printf("大端\n");
    }
    
    return 0;
}

8. this指针

8.1 this指针的引出

        为了很好的了解this指针,我们来通过定义一个日期类,并通过这个日期类来对this指针进行一个理解。

class Date
{ 
public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Print()
    {
        cout <<_year<< "-" <<_month << "-"<< _day <<endl;
    }
private:
    int _year;     // 年
    int _month;    // 月
    int _day;      // 日
};
int main()
{
    Date d1, d2;
    d1.Init(2022,1,11);
    d2.Init(2022, 1, 12);
    d1.Print();
    d2.Print();
    return 0;
}

对于上述日期类:Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函 数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?         C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏 的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编 译器自动完成

8.2 this指针的特性

        1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。

        2. 只能在“成员函数”的内部使用

        3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给 this形参。所以对象中不存储this指针。

        4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

8.3 C语言和C++实现栈的对比

        C语言实现

typedef int DataType;
typedef struct Stack {
    DataType* array;
    int capacity;
    int size;
} Stack;
void StackInit(Stack* ps) {
    assert(ps);
    ps->array = (DataType*)malloc(sizeof(DataType) * 3);
    if (NULL == ps->array) {
        assert(0);
        return;
    }
    ps->capacity = 3;
    ps->size = 0;
}
void StackDestroy(Stack* ps) {
    assert(ps);
    if (ps->array) {
        free(ps->array);
        ps->array = NULL;
        ps->capacity = 0;
        ps->size = 0;
    }
}
void CheckCapacity(Stack* ps) {
    if (ps->size == ps->capacity) {
        int newcapacity = ps->capacity * 2;
        DataType* temp =
            (DataType*)realloc(ps->array, newcapacity * sizeof(DataType));
        if (temp == NULL) {
            perror("realloc申请空间失败!!!");
            return;
        }
        ps->array = temp;
        ps->capacity = newcapacity;
    }
}
void StackPush(Stack* ps, DataType data) {
    assert(ps);
    CheckCapacity(ps);
    ps->array[ps->size] = data;
    ps->size++;
}
int StackEmpty(Stack* ps) {
    assert(ps);
    return 0 == ps->size;
}
void StackPop(Stack* ps) {
    if (StackEmpty(ps))
        return;
    ps->size--;
}
DataType StackTop(Stack* ps) {
    assert(!StackEmpty(ps));
    return ps->array[ps->size - 1];
}
int StackSize(Stack* ps) {
    assert(ps);
    return ps->size;
}
int main() {
    Stack s;
    StackInit(&s);
    StackPush(&s, 1);
    StackPush(&s, 2);
    StackPush(&s, 3);
    StackPush(&s, 4);
    printf("%d\n", StackTop(&s));
    printf("%d\n", StackSize(&s));
    StackPop(&s);
    StackPop(&s);
    printf("%d\n", StackTop(&s));
    printf("%d\n", StackSize(&s));
    StackDestroy(&s);
    return 0;
}

C++实现

typedef int DataType;
class Stack {
public:
    void Init() {
        _array = (DataType*)malloc(sizeof(DataType) * 3);
        if (NULL == _array) {
            perror("malloc申请空间失败!!!");
            return;
        }
        _capacity = 3;
        _size = 0;
    }
    void Push(DataType data) {
        CheckCapacity();
        _array[_size] = data;
        _size++;
    }
    void Pop() {
        if (Empty())
            return;
        _size--;
    }
    DataType Top() { return _array[_size - 1]; }
    int Empty() { return 0 == _size; }
    int Size() { return _size; }
    void Destroy() {
        if (_array) {
            free(_array);
            _array = NULL;
            _capacity = 0;
            _size = 0;
        }
    }

private:
    void CheckCapacity() {
        if (_size == _capacity) {
            int newcapacity = _capacity * 2;
            DataType* temp =
                (DataType*)realloc(_array, newcapacity * sizeof(DataType));
            if (temp == NULL) {
                perror("realloc申请空间失败!!!");
                return;
            }
            _array = temp;
            _capacity = newcapacity;
        }
    }

private:
    DataType* _array;
    int _capacity;
    int _size;
};
int main() {
    Stack s;
    s.Init();
    s.Push(1);
    s.Push(2);
    s.Push(3);
    s.Push(4);

    printf("%d\n", s.Top());
    printf("%d\n", s.Size());
    s.Pop();
    s.Pop();
    printf("%d\n", s.Top());
    printf("%d\n", s.Size());
    s.Destroy();
    return 0;
}

两者对比可以看出:

  •         在用C语言实现时,Stack相关操作函数有以下共性:
  •         每个函数的第一个参数都是Stack*
  •         函数中必须要对第一个参数检测,因为该参数可能会为NULL
  •         函数中都是通过Stack*参数操作栈的 调用时必须传递Stack结构体变量的地址

        结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据 的方式是分离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出错。

        C++中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制那些方法在 类外可以被调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。 而且每个方法不需要传递Stack*的参数了,编译器编译之后该参数会自动还原,即C++中 Stack * 参数是编译器维护的,C语言中需用用户自己维护。

9.类的六个默认成员函数

        一般来说如果一个类中什么成员都没有,即空类。生成空类,虽然其中什么成员都没有,但是会默认生成六个成员函数:

  1.         初始化和清理(构造函数和析构函数,其中构造函数主要完成初始化,析构函数完成清理工作),
  2.         拷贝复制(拷贝构造和赋值重载,拷贝构造使用同类对象初始化创建对象,赋值重载把一个对象赋值给另一个对象),
  3.         取地址重载(普通对象和const对象取地址)//一般不需要重新定义,编译器会默认生成,只有特殊情况才需要重载

10,构造函数

10.1 概念

      构造函数是一个特殊的成员函数,名字和类名相同,创建类类型对象时由编译器自动调用,以保证每个数据都有一个自己合适的初始化值,并且在对象整个生命周期只调用一次。

10.2 特性

        构造函数是一个特殊的成员函数吗,虽然名称叫做构造,但是主要任务并不是开空间创建对象,而是初始化对象。

具有如下特征:

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时编译器制动调用相应的构造函数
  4. 构造函数可以重载
  5. 如果类中没有显式定义构造函数,则编译器会自动生成一个无参的默认构造函数,一旦用户显式定义就不会生成
  6. 无参构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

编译器生成默认构造函数的作用:c++把类型分为内置类型和自定义类型,内置类型就是语言提供的数据类型,自定义类型就是我们自己定义的类型,当我们使用自定义类型的时候,编译器就会调用这个自定义类型成员调用它的默认成员函数,从而来对自定义类型进行初始化。在C++11中针对内置类型不初始化的缺陷打了补丁,内置类型成员变量在类中声明式可以给默认值。

10.2 构造函数整体赋值

        在创建对象时,编译器通过调用构造函数,给对象中的各个成员变量一个合适的初始值。如上述的日期类,但是调用构造函数后,对象中已经有一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称为初始化,因为初始化只能初始化一次,而构造体函数内可以多次赋值。

10.3 初始化列表

初始化列表,以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后边跟着一个括号,括号中为该成员变量的初始值或者表达式。

class Date
{
public:
    Date(int year, int month, int day)
        : _year(year)
        , _month(month)
        , _day(day)
    {}
 
private:
    int _year;
    int _month;
    int _day;
};

注意:

        1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

        2.类中包含引用成员变量,const成员变量,自定义类型成员变量,必须放在初始化列表位置进行初始化。

        3.尽量使用初始化列表进行初始化,对于自定义类型成员变量,一定会先试用初始化列表进行初始化。

        4.成员变量在类中的声明次序就是其在初始化列表的初始化顺序,与其在初始化列表的先后次序无关。

10.4 explicit关键字

        构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,具有类型转换的作用,接收单个参数的构造函数具体表现:

        1.构造函数只有一个参数

        2.构造函数有多个参数,除第一个参数没有默认值,其他参数都有默认值

        3.全缺省的构造函数

class Date {
public:
    // 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
    // explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
    explicit Date(int year) : _year(year) {}
    /*
    // 2.
   虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转
   换作用
    // explicit修饰构造函数,禁止类型转换
    explicit Date(int year, int month = 1, int day = 1)
    : _year(year)
    , _month(month)
    , _day(day)
    {}
    */
    Date& operator=(const Date& d) {
        if (this != &d) {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this;
    }

private:
    int _year;
    int _month;
    int _day;
};
void Test() {
    Date d1(2022);
    // 用一个整形变量给日期类型对象赋值
    // 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
    d1 = 2023;
    // 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转换的作用
}

,用explicit修饰构造函数,将会禁止构造函数的隐式转换。

11. 析构函数

11.1 概念

        析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是有编译器完成的,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。、

11.2 特性

        1.析构函数名是在类名前加一个字符 ~

        2.无参数返回值类型

        3.一个类只能有一个析构函数,如果未显式定义则会默认生成,析构函数不可以重载。

        4.对象生命周期结束时,编译系统就会自动调用析构函数。

        5.如果类中没有申请资源,析构函数可以不写,直接使用编译器默认生成的析构函数,但是如果有空间的申请一定需要写析构函数,否则会导致内存资源的泄露。

class Time {
public:
    ~Time() { cout << "~Time()" << endl; }

private:
    int _hour;
    int _minute;
    int _second;
};
class Date {
private:
    // 基本类型(内置类型)
    int _year = 1970;
    int _month = 1;
    int _day = 1;
    // 自定义类型
    Time _t;
};
int main() {
    Date d;
    return 0;
}
// 程序运行结束后输出:~Time()
// 在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
// 因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,
// _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;
// 而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:
// main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date
// 类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部
// 调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁main函数中并没有直接调用Time类析构函数,
// 而是显式调用编译器为Date类生成的默认析构函数
// 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

12.拷贝构造函数

12.1 概念

        拷贝构造函数只有单个形参,该形参是对本类型对象的引用(一般使用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

12.2 特征

        1.拷贝构造函数是构造函数的一个重载形式

        2.拷贝构造函数的参数只有一个且必须是类对象的引用,使用传值方式编译器会直接报错,会引发无穷递归调用,

        3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

        4.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝

                注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

        5. 拷贝构造函数典型调用场景:

  •                 使用已存在对象创建新对象
  •                 函数参数类型为类类型对象
  •                 函数返回值类型为类类型对象

        为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用 尽量使用引用。

13.赋值运算符重载

13.1 运算符重载

        为了增强代码的可读性,C++引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名字以及参数列表,其返回值类型和普通函数类似。

        函数名称为:关键字operator后边接需要重载的运算符符号。

        函数原型:返回值类型operator操作符(参数列表)

注意:

         不能通过连接其他符号来创建新的操作符:比如operator@

        重载操作符必须有一个类类型参数

        用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义

        作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐 藏的this

        .*   ::   sizeof   ?   : . 注意以上5个运算符不能重载。

13.2 赋值运算符重载

1. 赋值运算符重载格式

  •         参数类型:const T&,传递引用可以提高传参效率
  •         返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  •         检测是否自己给自己赋值
  •         返回*this :要复合连续赋值的含义
class Date {
public:
    Date(int year = 1900, int month = 1, int day = 1) {
        _year = year;
        _month = month;
        _day = day;
    }

    Date(const Date& d) {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    Date& operator=(const Date& d) {
        if (this != &d) {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }

        return *this;
    }

private:
    int _year;
    int _month;
    int _day;
};

2. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注 意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符 重载完成赋值。

13.3 前置++和后置++重载

class Date {
public:
    Date(int year = 1900, int month = 1, int day = 1) {
        _year = year;
        _month = month;
        _day = day;
    }
    // 前置++:返回+1之后的结果
    // 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
    Date& operator++() {
        _day += 1;
        return *this;
    }
    // 后置++:
    // 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
    // C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器
    自动传递
    // 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存
    一份,然后给this + 1
        //       而temp是临时对象,因此只能以值的方式返回,不能返回引用
        Date
        operator++(int) {
        Date temp(*this);
        _day += 1;
        return temp;
    }

private:
    int _year;
    int _month;
    int _day;
};

14. const 成员和static成员

14.1 const成员

        将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数 隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

1. const对象可以调用非const成员函数吗? 2. 非const对象可以调用const成员函数吗? 3. const成员函数内可以调用其它的非const成员函数吗? 4. 非const成员函数内可以调用其它的const成员函数吗?

解答:

  1. const 对象可以调用非 const 成员函数,但是在编译过程中会发出警告。因为 const 对象调用非 const 成员函数可能会导致对象状态的改变,违反了 const 对象的特性。

  2. 非 const 对象可以调用 const 成员函数,这是允许的。因为 const 成员函数不会修改对象的状态,所以非 const 对象调用 const 成员函数是安全的。

  3. const 成员函数内可以调用其他的非 const 成员函数,但是这样做会使得整个对象在调用期间被视为非 const。这是因为 const 成员函数虽然不能修改对象的成员变量,但是可以修改对象的 mutable 成员变量或者调用其他非 const 成员函数来修改对象的状态。

  4. 非 const 成员函数内可以调用 const 成员函数,这是完全合法的。因为非 const 成员函数可以修改对象的状态,包括调用其他 const 成员函数。调用 const 成员函数不会改变对象的状态,所以在非 const 成员函数内调用 const 成员函数是安全的。

14.2 static 成员

        概念:声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的 成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。

特性:

        1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区

        2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明

        3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问

        4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员

        5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

问题及解答:

        1.静态成员函数可以调用非静态成员函数吗?

                静态成员函数可以调用非静态成员函数,但是需要通过对象来调用非静态成员函数。因为静态成员函数没有 this 指针,无法直接访问非静态成员函数,所以需要通过对象来调用。

        2. 非静态成员函数可以调用类的静态成员函数吗

                非静态成员函数可以直接调用类的静态成员函数,不需要通过对象来调用。静态成员函数属于整个类,而不是某个特定对象,因此在非静态成员函数中可以直接访问类的静态成员函数。

        3.实现一个类,计算程序中创建出了多少个类对象(编程)

class A {
public:
    A() { ++_scount; }
    A(const A& t) { ++_scount; }
    ~A() { --_scount; }
    static int GetACount() { return _scount; }

private:
    static int _scount;
};
int A::_scount = 0;
void TestA() {
    cout << A::GetACount() << endl;
    A a1, a2;
    A a3(a1);
    cout << A::GetACount() << endl;
}

15. 友元函数和友元类

15.1 友元函数

        友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声 明,声明时需要加friend关键字。

class Date {
    friend ostream& operator<<(ostream& _cout, const Date& d);
    friend istream& operator>>(istream& _cin, Date& d);

public:
    Date(int year = 1900, int month = 1, int day = 1)
        : _year(year), _month(month), _day(day) {}

private:
    int _year;
    int _month;
    int _day;
};
ostream& operator<<(ostream& _cout, const Date& d) {
    _cout << d._year << "-" << d._month << "-" << d._day;
    return _cout;
}
istream& operator>>(istream& _cin, Date& d) {
    _cin >> d._year;
    _cin >> d._month;
    _cin >> d._day;
    return _cin;
}
int main() {
    Date d;
    cin >> d;
    cout << d << endl;
    return 0;
}

说明:

  •         友元函数可访问类的私有和保护成员,但不是类的成员函数
  •         友元函数不能用const修饰
  •         友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  •         一个函数可以是多个类的友元函数
  •         友元函数的调用与普通函数的调用原理相同

15.2 友元类

        友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

  •         友元关系是单向的,不具有交换性。 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  •         友元关系不能传递 如果B是A的友元,C是B的友元,则不能说明C时A的友元。
  •         友元关系不能继承,

16 内部类

        概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外 部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

        注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中 的所有成员。但是外部类不是内部类的友元。

        特性:

  1.         内部类可以定义在外部类的public、protected、private都是可以的。
  2.         注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3.         sizeof(外部类)=外部类,和内部类没有任何关系。
  • 17
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ღ星ღ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值