目录
前言:
c++作为一个面向对象的高级编程语言特点就是:封装,继承,多态。
类就是这三大特点的基础,所以说学好类至关重要。
在c++中的设计中,属性和行为写在一起,表现事物,也就命名为类
语法:class 类名{ 访问权限: 属性/行为 };
网上有很多类怎么去基础实现,这里不再进行补充,如果不会的可以看这篇文章 学习C++ 丨 类(Classes)的定义与实现!C/C++必学知识点! - 知乎 (zhihu.com)
也存在struct来写,但是不多,一般都是来用class
就比如我们写一个类,来求一个圆的周长;
#include <iostream>
using namespace std;
class Circle
{
public:
void Init(double r)
{
_r = r;
}
double print()
{
double C = _r * 2 * 3.14;
return C;
}
private:
double _r;
};
int main()
{
Circle s1;
s1.Init(10.0);
cout << s1.print() << endl;
return 0;
}
还是可以到达我们的基础要求的。
这里的private与public全是访问限定符
而访问限定符一共分为三种
1.public 共有 被修饰的成员在类外面还可以访问 ,这一点在上图可以验证
2.protected 被保护 作用范围到下一个结束或到 “}” 结束
3.private 私有 被修饰的成员在类外面不可以访问 以下给出截图验证
确实不能。
类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
但类里的毕竟全是函数,那么同样也可以做到声明与定义相分离,但是在分离后的定义需要在函数明前加上 类的名字:: 这一点的目的是告诉他去那可以找到声明;
1. 声明和定义全部放在类体中(需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。)
2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,(注意:成员函数名前需要加类名::)
就比如说我们把上面这个类进行定义与声明分离;
test.c
#include"test.h"
void Circle::Init(double r)
{
_r = r;
}
double Circle::print()
{
double C = _r * 2 * 3.14;
return C;
}
int main()
{
Circle s1;
s1.Init(10.0);
cout << s1.print() << endl;
return 0;
}
test.h
#pragma once
#include<iostream>
using namespace std;
using namespace std;
class Circle
{
public:
void Init(double r);
double print();
private:
double _r;
};
类的大小计算:
就比如我们上面这个类进行代码计算,
发现是8,但是如果我们定义一个结构体,同样也只放一个double类的变量,那么也同样是8;
然后经过多次验证,我们得知,类的大小计算大小是与存相关顺序相同类型的结构体大小是相同的。
特例:一个成员变量也没有,这是为了对于这个类的占位,就会使其大小为默认的1(表示该类存在);
同样只存一个char也是1,但意义完全不同;
this指针
我们观察上面的代码,发现public是作为公共区的,但是如果定义多个同样的类,但不同在空间,同时进行初始化或者打印时,这是又怎么分清时调用的谁呢?
这一点this指针便解决了这一问题,其实类里面函数还有一个隐藏的参数就是this指针,这个指针就是指向的类变量s1(上图的),就在表明init与print就是在调用的s1的成员变量,这一点编译器其实帮我们做了,如果我们自己补充就是如下:
当然百分百会报语法错误,因为编译器已经帮我们存放了一个隐藏参数this指针,我们在添加上,必是重复,
这个截图也在证明确实有隐藏参数this指针;
类的6个默认成员函数
上文就是类的最基础的知识,但是类仅仅做的了这些,那就有点瞧不起c++的祖师爷了;
其实如果我们定义了一个这样的空类:
class A
{
private:
int a;
};
int main()
{
return 0;
}
看似这个类里面什么成员函数也没有,实则不然,
关于如何证明确实存在六个默认成员函数的方法,目前我还是不知道,我查看了反汇编无法证明,如果有的话还请大佬指出,让小弟学习一下
如何查看反汇编
如何查看反汇编这里提一下,毕竟下面还是要通过反汇编来详细讲解六大默认成员函数的;
先按 F10
然后
就调出来反汇编了;
这里先展示六大默认成员函数的名字:
构造函数
构造函数的特点:
函数名于类名相同
无返回值
自动调用
可以进行重载(可以写多个,以便应对不同的需要)
构造函数也称为初始化函数;
在我们上面写的一个求圆周长的类里面就有一个初始化
但是c++祖师爷就说,如果这被称为构造函数,那就有点小瞧我的c++了,祖师爷设计的构造函数就是为我们省去了去调用初始化函数,编译器会自动调用
其大致代码实现如下:
class A
{
public:
A(int b = 5)//函数名一定要是与类的名一样
{
a = b;
}
private:
int a;
};
int main()
{
A a(2);//在此处进行初始化
return 0;
}
编译器也确实进行了自动调用
那么我们从反汇编来查看呢?
这里的mov就是 移动 将2加载到寄存器,
lea同样将操作数加载到寄存器内
call就是调用
还是可以观察到还是自动调用的;
但是针对于如此的函数,跟c差不了多少,还是无法体现c++的妙处,对此c++的祖师爷又进一步进行了扩展,将构造函数代码模板修改了这样:
这也就是平时所说的初始化列表:
注意:
1:每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2:类中包含以下成员,必须放在初始化列表位置进行初始化:
1引用成员变量
2const成员变量
3自定义类型成员(且该类没有默认构造函数时)
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使 用初始化列表初始化。
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
A(int b = 5,int d)//函数名一定要是与类的名一样
:a(b)
,c(d)
{
}
private:
int a;
int c;
};
int main()
{
A a(2,6);//在此处进行初始化
return 0;
}
要注意的是,第一个是:第二个是,没有加上;
圈红的地方是函数体,函数体内不是初始化,是简单的赋值;
但这个初始化的构造有一个特点,就是:无论构造内的顺序无论如何,他的初始化就是按照private的顺序来进行初始化的
就比如说:
还是按照先进行先初始化a,后初始化c,这就需要我们特别的注意
那么看完这些知识后,练习一个题吧:
设已经有A,B,C,D4个类的定义,程序中A,B,C,D构造函数调用顺序为?
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
通过观察一个全局对象,一个static修饰的静态对象,还有两个普通对象,从简单的来说肯定是先走a的构造后走b的。因为在走编译过程的时候都是从上而下所以肯定c是最先走构造函数的,最后在世d;所以构造顺序是 c a b d;
默认构造函数(由编译器自动生成):
这时候我们添加上打印,看看打印出来是什么效果
class A { public: void print() { cout << "a = " << a << endl; cout << "c = " << c << endl; } private: int a; int c; }; int main() { A a;//在此处进行初始化 a.print(); return 0; }
运行结果:
自动生成的默认构造函数的特点:
1.写了就不会生成,不写才会生成
2.构造函数必须调用
3.对内置成员类型不进行处理
(内置成员类型:int double char)
(自定义类型: 类)
4.与自己写的构造函数两者只可以存在一个。
当然构造函数还是存在无参,全缺省。
在定义是初始化也有点区别的。
析构函数
其实析构函数就是我们在写各种在堆上存储的数据,在使用完后,需要进行free的过程,对于析构的理解还是比较容易的,他跟构造一样还是自动调用,不需要我们特意的去指明要去调用
析构也分为:
默认生成的析构,与我们写的析构
析构的特点:
1.析构函数名为:~ + 类名
2.析构函数无参数,无返回值
3.析构的顺序是先定义的后析构(类的析构函数调用一般按照构造函数调用的相反顺序进行调用)
对于默认生成的析构函数的特点:
1.内置类型成员(内置类型成员就是我们常用的int类型,double类型,float.....类型)不会处理,比如我们在private中定义的int a;就不会析构。
2.自定义类型成员(自定义类型就是我们自己定义的类型,比如类类型)会调用其成员自己的析构
对于此我们在我们上面的类加上一个析构(添加一个数组)
class A
{
public:
A(int b,int d)//函数名一定要是与类的名一样
:c(d)
,a(b)
{
}
void print()
{
cout << "a = " << a << endl;
cout << "c = " << c << endl;
}
~A()
{
free(x);//我没有对x进行初始化,写的不是很完整,主要在体现析构
x = nullptr;
}
private:
int a;
int* x;
int c;
};
int main()
{
A a;//在此处进行初始化
a.print();
return 0;
}
我们看一下反汇编,(再多定义一个类类型的变量)
这也证明了先定义的类类型,后进行析构函数。
那我再将上一个题改一下问法:
设已经有A,B,C,D4个类的定义,程序中A,B,C,D构造函数调用顺序为?
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
上面已经解释了析构的顺序与构造的恰恰相反,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象。原本的构造顺序是c → a → b → d
所以我们顺序逆过来就是: d b a c,在此基础顺序上只需要注意修改static修饰的静态对象析构的顺序即可,也就是说需注意static改变对象的生存作用域之后,会放在局部 对象之后进行析构,然而全局对象就不一样了,他的作用范围可不仅仅再main的作用域里面,不可以保证出了main以后不用,所以c是再d之后析构。所以最后答案是 b a d c
拷贝构造:
定义:
拷贝构造就是效果如其名,就是进行copy
但是拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(注意:一般常用const修饰,是为了防止改变形参的值),然后再已经存在的类类型中进行创建新的对象进行copy
但实际上拷贝构造是构造函数的函数重载,这也就再说明拷贝构造的模板其实与构造函数没有多大的差别,再参数上一个是将要初始化的目的值,一个是类类型;
举例:
class A { public: A(int b,int d)//函数名一定要是与类的名一样 :c(d) ,a(b) { } A(const A& sub) { a = sub.a; c = sub.c; } void print() { cout << "a = " << a << endl; cout << "c = " << c << endl; } ~A() { free(x);//我没有对x进行初始化,写的不是很完整,主要在体现析构 x = nullptr; } private: int a; int* x; int c; }; int main() { A a(1,2);//在此处进行初始化 A b(a); a.print(); b.print(); return 0; }
特点:
1.拷贝构造就是构造函数的一种重载函数,。
2.构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。
3.拷贝构造的参数只有一个,并且这个参数是类类型的引用(使用传值方式编译器直接报错,因为会引发无穷递归调用)是因为传值的话,还会再开辟一个空间去拷贝这个形参,但是这个过程还会进行调用拷贝构造,然后依次类推还会调用拷贝构造,会无限循环。
比如就像这样:
对此还有编译器自动生成的默认拷贝函数:
默认生成的拷贝构造函数就是进行简单的值拷贝,并且还有以下特点:
1.内置类型成员会简单进行值拷贝
2.自定义类型成员会调用在自己的成员的拷贝构造函数
这就会导致以下这个问题(在注意事项中提及)
注意事项:
比如说栈的拷贝:
我们知道栈的实现一般会有三个成员变量:
top
capacity
int* a;
这三个变量前两个进行值拷贝完全没有问题,但是问题出就出在第三个,第三个如果还是值拷贝的话就会引发下面这个问题:
这就会导致两个类类型的a全指向同一块空间,这就会导致如果对一个类类型对a 进行修改,就会导致对第二个类类型造成影响。显然这样会导致出现很多不必要的问题,对于此,就需要进行深拷贝。
深拷贝:
用栈进行实现:
class stack { public: stack() { a = nullptr; top = capacity = 0; } stack(const stack& ps) { a = (int*)malloc(sizeof(int) * (ps.capacity)); if (a == NULL) { perror("malloc fail"); return; } memcpy(a, ps.a, sizeof(int) * (ps.top)); top = ps.top; capacity = ps.capacity; } void push(int x) { //... } void StackPop() { //... } int StackTop() { //... } int StackEmpty()//空返回1,非空返回0 { } ~stack() { a = NULL; top = capacity = 0; } private: int* a; int top; int capacity; };
赋值重载:
赋值与拷贝就是差不多,但是赋值重载就是纯纯简单的赋值再加一下特殊的像拷贝构造那样的情况。
其实情况与拷贝大差不差。
class A
{
public:
A(int b,int d)//函数名一定要是与类的名一样
:c(d)
,a(b)
{
}
A(const A& sub)
{
a = sub.a;
c = sub.c;
}
A& operator=(const A& d)
{
if (this != &d)
{
a = d.a;
c = d.c;
}
return *this;
}
~A()
{
free(x);//我没有对x进行初始化,写的不是很完整,主要在体现析构
x = nullptr;
}
private:
int a;
int c;
};
特殊案例便不再展示别的
运算符重载
运算符重载是具有特殊函数名的函数 ,可以帮助实现自定义类型的大小比较
(千万要记得这是运算符,不是关键字)
函数名字为:关键字operator后面接需要重载的运算符符号。(如:operator+)
注意事项:
1.只能重载C++中已有的运算符,不能通过连接符号创建新的操作符。例如,operator@
是非法的,因为 @
不是C++中的有效运算符。
2.重载操作符必须有一个类类型(或枚举类型)参数,这是为了防止修改内置类型的运算符行为
3.用于内置类型的运算符,其含义不能改变(如:内置类型的 + 不能改变它原有的含义),你不能自己定义一个+,但是效果确实 减 。
4.作为类成员函数重载时,其形参看起来比操作数数目少1(因为成员函数的第一个参数为隐藏的this)
5. (.*(成员指针访问运算符))(::
(作用域解析运算符))(sizeof
(大小运算符)(?:
(三元条件运算符))(.
(成员访问运算符))注意以上5个运算符不能重载(这个经常在笔试选择题中出现)
这里对于运算符的讲解不在多描述,个人还是感觉还是很简单的,
运算符重载又一大特点就是:只要写了一个完整的,就可以通过复用,来实现别的相似作用的运算符重载。
实现起来还是比较容易的,不在说明;
下面展示一下利用日期类实现各种的运算符重载
test.c
#include"test.h" date::date(int year, int month, int day) { _year = year; _month = month; _day = day; } date::date(const date& d) { //cout << "date(date& d)" << endl; _year = d._year; _month = d._month; _day = d._day; } bool date::operator<(date& d) { return _year < d._year || ((_year == d._year) && (_month < d._month)) || ((_year == d._year) && (_month == d._month) && (_day < d._day)); } bool date::operator==(date& d) { return _year == d._year && _month == d._month && _day == d._day; } bool date::operator!=(date& d) { return !(*this == d); } bool date::operator<=(date& d) { return *this == d || *this < d; } int date::Get_month_day(int year, int month) { int month_day[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { return 29; } return month_day[month]; } date date::operator += (int day) { if (day < 0) { return *this -= (-day); } _day += day; while (_day > Get_month_day(_year, _month)) { _day -= Get_month_day(_year, _month); ++_month; if (_month == 13) { _year++; _month = 1; } } return *this; } date date::operator + (int day) { if (day < 0) { return *this - (-day); } date tmp(*this); tmp += 50; return tmp; //date tmp(*this); //tmp._day += day; //while (tmp._day > Get_month_day(tmp._year, tmp._month)) //{ // tmp._day -= Get_month_day(tmp._year, tmp._month); // ++tmp._month; // if (tmp._month == 13) // { // tmp._year++; // tmp._month = 1; // } //} //return tmp; } date date::operator-=(int day) { if (day < 0) { return *this += (-day); } _day -= day; while (_day < 0) { --_month; _day += Get_month_day(_year, _month); if (_month == 0) { --_year; _month = 12; } } return *this; } date date::operator-(int day) { if (day < 0) { return *this + (-day); } date tmp(*this); tmp -= day; return tmp; } //日期 减 日期 int date::operator-(const date& d) { date max = *this; date min = d; int flag = 1; if (max < min) { flag = -1; max = d; min = *this; } int n = 0; while (max != min) { ++min; ++n; } return n * flag; } date date::operator++()//前置 { *this += 1; return *this; } date date::operator++(int)//后置 { date tmp(*this); *this += 1; return tmp; } date date::operator--() { //this->operator-=(1); *this -= 1;//this为什么有时候加*,有时候不加?//2 return *this; } date date::operator--(int) { date tmp(*this); *this -= 1; return tmp; } void date::print() { cout << _year << "/" << _month << "/" << _day << endl; }
test.h
class date { public: date(int year = 2, int month = 2, int day = 2); date(const date& d); bool operator<(date& d); bool operator==(date& d); bool operator!=(date& d); bool operator<=(date& d); int Get_month_day(int year, int month); date operator += (int day); date operator + (int day); date operator-=(int day); date operator-(int day); //日期 减 日期 int operator-(const date& d); date operator++();//前置 date operator++(int);//后置 //赋值 date& operator=(const date& d); date operator--(); date operator--(int); void print(); private: int _year; int _month; int _day; };
带有const修饰的成员函数
我们上面已经提到,类成员函数里面每个函数的参数都含有一个隐藏的参数就是this指针,这个指针如果我们需要达到要求,使得我们不可以对其进行修改,那么怎么去修改函数呢,
仔细想想,this指针是隐藏的参数,而且我们自己添加上就会使其重复,导致犯语法错误,这时候c++祖师爷就对此进行了优化,在函数末尾添加const进行修饰,就是表达const在修饰this指针。
添加const
不添加const
取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
自己生成:
class A { public: A* operator&() { return this; } const A* operator&()const { return this; } private: int a; };
默认生成:
struct B { private: int _b = 4; }; int main() { B b; cout << &b << endl; return 0; }
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需 要重载,比如想让别人获取到指定的内容!
扩充部分:
explicit关键字
explicit关键字在写程序时使用的次数较少,但是仔细观察会发现,在C++标准库中的相关类声明中explicit出现的频率是很高的,那么explicit关键字到底有什么作用呢?接下来我就为大家一一解答.
explicit为清晰的;明确的之意.其作用便是:关键字explicit可以阻止隐式转换的发生。
比如: 单参数构造函数的隐式转换
单参数构造函数不仅可以用于构造对象,还可以用于隐式类型转换。具体来说,当构造函数满足以下条件之一时,它可以用于隐式类型转换:
1. 构造函数只有一个参数
2. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
3. 全缺省构造函数
在这种情况下,编译器可以自动将参数类型转换为类类型。 只有一个参数,那么这个参数承担着两种角色:
1.用于构建单参数的类对象.
2.隐含的类型转换操作符.
- 例如:一个类A的构造函数A(int i)就是,既可以用来作为构造器,又可以实现隐式转换A a=1;因为1可以通过构造函数A(int i)转换为一个类A的对象。(隐含的类型转换操作符)
如果还是不是很清楚,那么我们从定义与使用比较上来解释:
首先来看隐式转换的定义:
隐式转换是指在编程语言中自动将一种数据类型转换为另一种数据类型的过程,而不需要程序员显式地进行转换操作。隐式转换可以导致意想不到的结果和错误,因此某些编程语言提供了关键字(如explicit)来阻止隐式转换的发生。
例如,在C++中,通过使用关键字explicit来声明构造函数可以阻止隐式转换的发生。下面是一个示例:
#include <iostream>
class MyClass {
public:
explicit MyClass(int value) : m_value(value) {
}
int getValue() {
return m_value;
}
private:
int m_value;
};
void printValue(MyClass obj) {
std::cout << "Value: " << obj.getValue() << std::endl;
}
int main() {
// MyClass obj1 = 10; // 这行代码会导致编译错误,因为explicit关键字阻止了int到MyClass的隐式转换
MyClass obj2(20); // 显式地创建一个MyClass对象
printValue(obj2);
return 0;
}
在上面的示例中,使用了关键字explicit来声明构造函数,阻止了int到MyClass的隐式转换。如果尝试编写类似于 MyClass obj1 = 10;
的代码,编译器将会报错。通过显式地调用构造函数MyClass obj2(20);
,则可以避免隐式转换的发生。
在比如说多参数的情况下:
class Date
{
public:
// 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用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);
}
int main()
{
Test();
return 0;
}
走完程序后会将d1的year初始化给定的2022;
当然我们使用了explicit修饰,已经不允许隐式类型转化如果直接 d1=2022,是不允许的,但当explicit删除后,就可以了;
总的来说:用explicit修饰构造函数,将会禁止构造函数的隐式转换
如果想进行更深的了解
参考文献:
隐式转换的问题
隐式转换虽然方便,但也可能导致意想不到的行为和错误。例如:
void func(const A& a) {
// 一些操作
}
int main() {
func(10); // 隐式转换:将 int 转换为 A 类型
return 0;
}
-
这里,
func(10);
会隐式调用A(int i)
构造函数,将10
转换为A
类型的对象。这种行为可能会导致代码难以理解和维护。
static成员
概念:
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的 成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
注意:
1:静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2:静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3:类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4:静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5::静态成员也是类的成员,受public、protected、private 访问限定符的限制
由此可以写一段代码,计算该类创建了多少个对象:
#include<iostream> using namespace std;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; //或者int MyClass::count = 0; // 在类外定义静态成员变量,不添加 static 关键字 void TestA() { cout << A::GetACount() << endl; A a1, a2; A a3(a1); cout << A::GetACount() << endl; } int main() { TestA(); return 0; }
运行结果:
(开始0个,最后三个)(运行结果符合设想要求)
问题一:静态成员函数可以调用非静态成员函数吗?
问题二:非静态成员函数可以调用类的静态成员函数吗?
这里我们用运行结果来验证:
1:
class A { public: A() { ++_scount; } A(const A& t) { ++_scount; } ~A() { --_scount; } static int GetACount() { A(_count); return _scount; } private: static int _scount; }; int A::_scount = 0; //或者int MyClass::count = 0; // 在类外定义静态成员变量,不添加 static 关键字 void TestA() { cout << A::GetACount() << endl; A a1, a2; A a3(a1); cout << A::GetACount() << endl; } int main() { TestA(); return 0; }
我们先在类外定义静态成员变量为0,然后在静态成员变量里面调用函数A(const A& t) { ++_scount; },如果可以调用,那么第一个打印的值应该是1;反之不能编译通过;
运行结果:
所以问题一的答案是:静态成员函数可以调用非静态成员函数
2:
class A { public: A() { A::GetACount(); }//或者GetACount();都可以 A(const A& t) { A::GetACount(); } ~A() { --_scount; } static int GetACount() { ++_scount; return _scount; } private: static int _scount; }; int A::_scount = 0; //或者int MyClass::count = 0; // 在类外定义静态成员变量,不添加 static 关键字 void TestA() { cout << A::GetACount() << endl; A a1, a2; A a3(a1); cout << A::GetACount() << endl; } int main() { TestA(); return 0; }
在本代码,取消了在构造函数里面的++scount,而是将++scount转移到静态成员变量里面。开始先调用A::GetACount(),那么此时_scount为1;最后a1++,a2++,a3++,最后在走一次A::_scount;结果应该为5;
运行结果:
所以问题二答案为:非静态成员函数可以调用类的静态成员函数。
友元
定义:
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
分类:
友元分为:友元函数和友元类
友元函数:
当你想为一个类重载 operator<<
,使其能用 cout << myObject
这种形式输出对象时,你不能直接将其作为成员函数来实现。 这是因为:
- 成员函数的第一个隐含参数是
this
指针,指向调用该函数的对象。 - 对于
cout << myObject
,我们希望cout
(一个ostream
对象) 作为左操作数,myObject
作为右操作数。 - 如果
operator<<
是成员函数,this
指针会绑定到myObject
,而cout
则无法成为左操作数。 - 对于此我们可以使用,全局函数重载: 将
operator<<
重载为全局函数,这样就可以显式地控制两个操作数的顺序。 函数原型通常是ostream& operator<<(ostream& os, const MyClass& obj)
。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
因为友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加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;
}
注意
1:友元函数可访问类的私有和保护成员,但不是类的成员函数
2:友元函数不能用const修饰
3:友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4:一个函数可以是多个类的友元函数
5:友元函数的调用与普通函数的调用原理相同
友元类:
概念:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。( 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。)
友元关系不能传递 如果B是A的友元,C是B的友元,则不能说明C时A的友元。
友元关系不能继承。
class Time { friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成 //员变量 public: Time(int hour = 0, int minute = 0, int second = 0) : _hour(hour) , _minute(minute) , _second(second) {} private: int _hour; int _minute; int _second; }; class Date { public: Date(int year = 1900, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} void SetTimeOfDate(int hour, int minute, int second) { // 直接访问时间类私有的成员变量 _t._hour = hour; _t._minute = minute; _t._second = second; } private: int _year; int _month; int _day; Time _t; }
在这段代码中,Time类声明了Date类为其友元类,这意味着Date类可以直接访问Time类中的私有成员变量。因此,在这里,Time类是Date类的友元类。
内部类
概念:
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外 部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中 的所有成员。但是外部类不是内部类的友元。
特点:
内部类可以定义在外部类的public、protected、private都是可以的。
注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
sizeof(外部类)=外部类,和内部类没有任何关系。
简单的来说就是类里面嵌套类;
那个简单的举例:
class A { public: A(int a) :_a(a) {} class B // B天生就是A的友元 { public: void foo(const A& a) { cout << a._a << endl; } }; private: int _a; }; int main() { A::B b; b.foo(A(1)); return 0; }
运行结果:
在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性, 那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
到这里就已经完了,c++已经入门了,可以正常使用类了;