在之前所写的日期类中已经包含了类和对象的大部分知识点,但是任然有1欠缺下面便是类和对象的一些欠缺知识点。
const修饰的对象
下面是一个日期类中的打印函数
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
假设这里有一个日期类它被const修饰了,然后我在调用这个const对象的print函数会报错吗?答案是肯定的,因为const 修饰的日期类指针类型为const date* ,而非const修饰的就是date* ,如果这个函数能够正常的调用那么就可以在这个函数内部去修改const修饰的对象,这是不符合规则的。所以这里会报错。如何解决呢?也就是写一份this指针类型为const date*的函数就可以了例如下面这样。
void Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
但是很显然在日期类的这里写两份函数是没有意义的,那么在什么时候函数要加const呢?
即你所写的函数不改变对象本身值的情况下可以加const修饰,而且只用写一份const函数,那么非const对象可以调用(权限缩小),cosnt对象也可以调用(权限平移)。
那么在什么情况下要写两份函数(一份const,一份非const)呢?
以一个顺序表为例子下面我会重载顺序表的[]符号让其能够直接访问顺序表中的值。
例如下面:
class seqList
{
public:
seqList(int capacity = 10)
{
_a = (int*)malloc(sizeof(int) * (capacity));
if (_a == NULL)
{
perror("malloc fain");
exit(-1);
}
_capacity = 4;
_size = 0;//指向的是尾端元素的下一个
}
void push(int x)
{
//这里需要判断是否需要扩容但这里我就不写了
_a[_size] = x;
_size++;
}
size_t size() const
{
return _size;
}
//下面我要重在[]让其和数组一样
int& operator[](int x) const
{
assert(_size > x);//防止出现越界访问
return _a[x];
}//因为数组的[]是能够改变数组中的值的所以这里返回的是这_a[x]的引用
//需要注意在语法层面引用并不会额外开辟空间,但是从底层实现的角度上引用是开辟了空间的
//这一份用于const修饰的对象使用
int& operator[](int x)
{
assert(_size > x);//防止出现越界访问
return _a[x];
}//这一份用于非const修饰的对象调用
private:
int* _a;
int _size;
int _capacity;
};
void printf(const seqList& s)
//因为这里的s为const修饰的所以需要重新写一个[]函数,用于const类型
{
for (int i = 0; i < s.size(); i++)
{
cout << s[i] << " " << endl;
//这里不需要修改s[i]
}
cout << endl;
}//这里在函数体外面写一个打印函数功能为输出顺序表的值为防止有人修改这里的s要使用const修饰
void test3()
{
seqList l1;
l1.push(1);
l1.push(2);
l1.push(3);
l1.push(4);
for (int i = 0; i < 4; i++)
{
cout<<l1[i];
l1[i]++;//这里的[]既能获取值又能改变值
}//这里的l1为非const类型所以不需要const类型的函数,
cout << endl;
seqList l2;
l2.push(1);
l2.push(2);
l2.push(3);
l2.push(4);
printf(l2);//这里的l2过去就变成了const修饰,所以需要写两个[]重载函数
}
总结:在类中,当你希望有两个版本的函数,一个用于修改对象的状态,另一个用于只读操作时,你可能会同时提供一个const修饰的函数和一个非const修饰的函数。这种情况下,这两个函数通常具有相同的名称和参数列表,但一个被声明为const,另一个不是。
上面就是在printf函数中我只需要读取数据,而普通的[]函数即可读又可以写。
初始化列表
在类和对象(中)这篇文章中我提到过一个myqueue类,这个类由两个stack组成,对于这个myqueue类不需要写构造,析构函数。因为编译器自动生成的构造和析构函数对于自定义类型会去调用自定义类型的构造和析构函数,但是如果stack类中也没有写构造函数呢?在这种情况下也就只能使用初始化列表了。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟
一个放在括号中的初始值或表达式。
【注意】
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化: 引用成员变量 const成员变量 自定义类型成员(且该类没有默认构造函数时)
下面就以一个普通的A类为例子解释
class B
{
public:
B(int k)
{
_c = k;
}
private:
int _c;
};//这里的B类我没有写默认构造函数(即就算无参数也能运行的构造函数)
//然后在下面的A类中又包含B类这个时候就可以使用初始化列表
class A
{
A(int a, int b, int c)
:k1(c)
,_a(a)
,_b(b)
{
}
private:
B k1;
int _a;
int _b;
};
例如这种,当然初始化列表和构造函数是可以混用的。例如下面这样
class stack
{
public:
stack(int x)
{
_a = (int*)malloc(sizeof(int) * x);
_top = 0;
_capacity = x;
}
private:
int* _a;
int _top;
int _capacity;
};//依旧和上面一样在这个不完善的类中没有默认构造函数
class myqueue
{
public:
myqueue(int m = 5,int n = 5)
:a1(m)
,a2(n)
{
k = 10;
}
private:
stack a1;
int k = 8;
stack a2;
};
int main() {
myqueue b1(10,10);
return 0;
}
在b1对象中k最后的值也会变成10。那么下面来解释为什么初始化列表能够达成这样的功能呢?
首先要明确在一个类中的private域里面的成员变量都只是声明,即声明在这个类中存在这个成员变量,但是并没有实例化,而初始化列表就是定义的地方。即将private里面的变量实例化,所以即使你不使用初始化列表,在底层编译器也会自己去调用初始化列表。因此尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。而对于c++11中的新语法也就是支持在类中的private域中使用缺省值,这个缺省值也是提供给初始化列表。使用的。例如上卖弄的k = 8,如果我在myqueu的默认构造函数中没有给k赋值,那么k最后的值也即是8。
其次最后一点:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
即在初始化类列表实例化变量的时候,是按照private域中的顺序进行的。
匿名对象
在写一些函数的时候有时候需要我们去传入一个对象,但是这个函数中又不需要使用这个对象,完全是因为函数需要使用一个对象去调用而已,那么在这种情况下就可以使用匿名对象,匿名对象有一个特点那就是生命周期只在那一行,但是使用const能够延长匿名对象的生命周期。
例如下面:
class A
{
public:
A(int x = 10)
{
_a = 10;
}
A(A& a)
{
_a = a._a;
}
~A()
{
cout << "析构A类对象" << endl;
}
private:
int _a;
};
void func(const A& a)//因为这个函数不涉及到a的改变即可以使用const和引用
{
cout << "匿名类" << endl;
}
int main()
{
//如果要使用这个func函数,那么其中一种方法也即是创建一个类A对象
A aa;
func(aa);
cout << "-------------------------" << endl;
A();
//第二种方法去调用func函数
cout << "--------------------------" << endl;
func(A());
const A&ca = A();
cout << "呵呵呵呵" << endl;
return 0;
}
从中能够看到匿名对象的生命周期确实只有那一行,例如31行处直接调用了析构,但是第38行经过const之后很明显匿名对象的生命周期延长了,因为都已经打印出呵呵了,但任然没有调用析构函数。
关于匿名对象的使用下面还有一些场景,但是需要了解了隐式类型转换之后才能更好的理解。
explicit关键字
隐式类型转换
在讲这个关键字之前要先了解什么使隐式类型转换。
例如下面的类
class A
{
public:
A(A& a)
{
_a = a._a;
}
A(int x = 10)
{
_a = x;
}
private:
int _a;
};
int main()
{
A a(1);//可以这样去实例化对象
A b = 2;//也可以这样去实例化对象
//那么这里是怎么运行的呢?以 = 为例,首先会去调用构造函数使用2去构造一个对象,
//再使用拷贝构造函数拷贝给b,但是编译器会优化也就是直接使用
//2去构造b
return 0;
}
那么怎么去验证编译器做了优化呢?可以使用下面的代码验证。
class A
{
public:
A(A& a)
:_a(a._a)
{
}//使用初始化列表去完成拷贝构造函数和默认构造函数
A(int x = 10)
:_a(x)
{
}
private:
int _a;
};
int main()
{
const A& b = 2;//那么如果这里在引用的前面不使用const是会报错的,为什么呢?
//因为当使用2去构造一个临时对象的时候,临时对象具有常性,所以你要使用引用那就必须加上const
//因为要加const才能使用引用,这一点证明了是通过2去构造临时变量,在使用临时变量去拷贝构造的
return 0;
}
可以知道在const A&b那一行编译器没有进行优化,而在上面的代码中连续的构造函数就会被直接优化为使用x去创建变量。那么这个功能有何用呢?这个功能的其中一个使用场景也即是当顺序表里面储存的不是int而是自定义类型变量。例如下面:
//了解隐式类型转化的使用场景
class slist
{
public:
slist(int x = 10)
:a ((A*)malloc(sizeof(A) * x))
,_size(0)
,_capacity(10)
{
}//使用初始化列表去写默认构造函数
slist(slist& a)
:a(a.a),
_size(a._size),
_capacity(a._capacity)
{
}//拷贝函数
const A& operator[](int x)const//只可读
{
return a[x];
}
A& operator[](int x)//可读可写
{
return a[x];
}
private:
A* a;
int _size;
int _capacity;
};//定义了一个顺序表类
int main()
{
//那么给顺序表赋值就有三种方法
//方法一:
A a(1);
A a1(2);
A a3(3);
A a4(4);
int i = 0;
slist aa;
aa[i++] = a;
aa[i++] = a1;
aa[i++] = a3;
//.....
//方法二使用匿名对象
slist bb;
bb[0] = A(1);
//方法三直接使用了隐式类型转换
for (int i = 0; i < 4; i++)
{
bb[i] = i;//这里原来的顺序也就是先使用i去构造A临时对象,在使用拷贝构造赋值给一个类A对象,最后使用[]函数赋值给了bb对象
//里面的数组
}
}
那么这个explicit关键字就是加在构造函数上面,当加了这个关键字之后便不会支持隐式类型转换了。
例如下面这样
class A
{
public:
A(A& a)
:_a(a._a)
{
}//使用初始化列表去完成拷贝构造函数和默认构造函数
explicit A(int x = 10)
:_a(x)
{
}
private:
int _a;
};
然后就会看到编译报错了。
上面都是对于单个参数的隐式类型转换c++11已经支持多个参数的隐式类型转换了。
class B
{
public:
B(int x = 10, int y = 10)
:_a(x),
_b(y)
{
}
private:
int _a;
int _b;
};
int main()
{
B a = { 12,13 };//使用{}来完成多参数的隐式类型转换
return 0;
}
static成员
首先我们来看一道题目:实现一个类,计算程序中创建出了多少个类对象。
这道题目有两个解法解法一:全局变量
例如下面
int sum = 0;//用于记录一共创建过多少个变量
int sum1 = 0;//用于记录还有多少可用的对象
class A
{
public:
A()
{
_a = 10;
sum++;
sum1++;
}
~A()
{
sum1--;//因为sum1记录的是还剩多少可用对象,当一个对象销毁的时候sum1自然要减减
}
private:
int _a;
};
int main()
{
A a;
A b;
A c;
A();
cout << sum << " " << sum1 << endl;
return 0;
}
但是这么写有一个坏处那就是,sum和sum1在外部是可修改的,如果有人修改了sum的值那就会出现错误。那么这个时候就可以使用方法二了
方法二:使用static 成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
例如下面:
class A
{
public:
A()
{
_a = 10;
sum++;
sum1++;
}
~A()
{
sum1--;//因为sum1记录的是还剩多少可用对象,当一个对象销毁的时候sum1自然要减减
}
static void print()
{
cout << sum << " " << sum1 << endl;
}//静态成员函数,和普通成员函数不同的一点也即是没有this指针,所以、
//在外面可以不使用对象去访问而直接使用类去访问。
private:
int _a;
static int sum;//不能在这里为sum初始化因为sum储存在静态区,而不是栈区所以需要在外面初始化
static int sum1;
};//注意sum为所有类对象共有的,和函数一样都只会保存一份
int A::sum1 = 0;
int A::sum = 0;
int main()
{
A a1;
A a2;
A a3;
A();
A::print();//因为是静态成员函数所以可以直接使用类去访问
return 0;
}
下面是特性:
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
友元函数
首先让我们来看一下日期类里面的流插入(<<)运算符重载。首先如果我们是在类里面去实现这个函数。
#include<iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out,const Date& d1);
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& a)
{
_year = a._year;
_month = a._month;
_day = a._day;
}
//ostream& operator<<(ostream& out) const//因为这里不需要去修改this指针的值所以这里使用const修饰
//{
// out << "year:" << _year <<" " << "month:" << _month <<" " << "day:" << _day << endl;
// return out;
//}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out,const Date& d1) //因为这里不需要去修改d1的值
//所以这里使用const修饰
{
out << "year:" << d1._year << " " << "month:" << d1._month << " " << "day:" << d1._day << endl;
return out;
}
int main()
{
Date d1(2022, 7, 12);
//cout << d1;//这么写会发现直接报错了,为什么呢?因为在重载<<函数里面
//前面一个操作数为this指针也就意味着d1必须放在前面
//d1 << cout;//这么写就可以,但是这很明显并不符合使用这个操作符的习惯,
//所以为了完成这个函数重载必须的使用友元函数
cout << d1;
return 0;
}
如果你使用友元函数,那么实现的这个操作符就只能d1<<cout这么去使用,但是使用友元函数就可以修改类的位置,让cout<<d1能正常的使用。
经过上面的代码能够看到友元函数的特点:
1.友元函数可访问类的私有和保护成员,但不是类的成员函数
2.友元函数不能用const修饰 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
3.一个函数可以是多个类的友元函数 友元函数的调用与普通函数的调用原理相同
友元类
假设这里有两个类,一个是日期类,一个是简单的时间类。那么如果我想在日期类的对象可以直接访问时间类
#include<iostream>
using namespace std;
class ti
{
friend class Date;//声明日期类为时间类的友元类
//那么日期类就可以访问时间类的所有成员和函数
public:
//......函数暂时忽略
ti(int x = 10)
{
timei = x;
}
private:
int timei;
};
class Date
{
friend ostream& operator<<(ostream& out,const Date& d1);
public:
Date(int year = 1, int month = 1, int day = 1,int time = 10)
:_year(year),
_month(month),
_day(day),
d1(time)
{
}
Date(Date& a)
{
_year = a._year;
_month = a._month;
_day = a._day;
}
void print()
{
cout << d1.timei << endl;
}
private:
int _year;
int _month;
int _day;
ti d1;
};
ostream& operator<<(ostream& out,const Date& d1) //因为这里不需要去修改this指针的值所以
//这里使用const修饰
{
out << "year:" << d1._year << " " << "month:" << d1._month << " " << "day:" << d1._day << endl;
return out;
}
int main()
{
//我若想在日期类中可以直接使用时间类那么就可以将时间类设置为日期类的友元类
Date d1(2022, 7, 7);
cout << d1;
d1.print();
}
从上面就可以看出友元类的特点:也就是能够访问另一个类的私有成员。
希望这篇博客能对你有所帮助。如果有错误欢迎指出。