目录
本次内容大纲:
1.再谈构造函数
1.1 构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
//构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值
class Date
{
public:
//构造函数
Date(int year, int month, int day)
{
_year = year; //第一次赋值
_year = 2024; //第二次赋值
//.....多次赋值
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
在函数体中,我们可以多次给成员对象赋值,而不是初始化
1.2 初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
//初始化列表
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 9, 15);
return 0;
}
注意:
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时)
引用成员变量和const成员变量都要一个相同的特点,在定义时必须初始化,所以它们只能使用初始化列表初始化
引用创建时必须初始化
int& a; //error
int b = 10;
int& a = b; //创建就初始化
const变量创建时必须初始化
const int a; //error 创建时未初始化
const int a = 10; //创建时初始化
自定义成员:
若类中没有默认构造函数,我们实例化该类对象时应该传参对其初始化,即:初始化没有默认构造函数的类对象时,必须使用初始化列表来对其进行初始化
在这里说明一下,默认构造函数是指不用传参就可以调用的构造函数:
1.我们不写构造函数,编译器自己生成的
2.无参的构造函数
3.全缺省的构造函数
typedef int DateType;
class Stack
{
public:
//构造函数
Stack(int capacity)
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
if(nullptr == _a)
{
perror("Stack::malloc");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
//析构函数
~Stack()
{
if (_a)
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
}
private:
int* _a;
int _top;
int _capacity;
};
class A
{
public:
A(int& ref, int a)
:_ref(ref)
,_a(a)
,_st(10)
{}
private:
int& _ref; //引用
const int _a = 2; //const
Stack _st; //没有默认构造函数
};
int main()
{
int p = 10;
A a(p, 20);
return 0;
}
在定义时就得初始化得变量类型,就必须放在初始化列表进行初始化
还有一个要注意得是,可以看到在构造函数的参数中其中ref引用的意义是防止野引用
如果这里是int ref的话,将一个局部变量给_ref初始化,就会导致_ref引用到一个被释放的空间
在类中声明时给值const int _a = 2;,这个意思是给初始化列表缺省值,如果我们没有在初始化列表对_a进操作,_a就会被赋值为2
3. 尽量使用初始化列表初始化
当你实例化一个对象时,初始化列表就是对象成员定义的地方,无论你是否使用初始化列表,每个对象成员都会走一遍初始化列表(成员变量需要定义出来)
对于内置类型,使用初始化列表和在构造函数体内初始化实际上没有什么区别,其差别就类似以下代码:
//使用初始化列表初始化
int a = 10;
//使用函数体初始化(没有使用初始化列表)
int a;
a = 10;
对于自定义类型,使用初始化列表还可以提高代码效率
class Time
{
public:
Time(int hour = 1)
{
_hour = hour;
}
private:
int _hour;
};
class test
{
public:
test()
:_t(12) //调用拷贝构造函数
{}
private:
Time _t;
};
在类test中,我们调用了一次类Time中的构造函数
在不使用初始化列表的情况下:
class Time
{
public:
Time(int hour = 1)
{
_hour = hour;
}
private:
int _hour;
};
class test
{
public:
test(int hour)
{
//初始化列表调用了一次构造函数(不使用初始化列表也会走这个流程)
Time tmp(hour); //调用一次构造函数
_t = tmp; //调用赋值运算符重载函数
}
private:
Time _t;
};
与上面那段代码对比,这里多调用一次构造函数和赋值运算符重载函数
初始化野不能解决我们百分之百的初始化问题
例如:
情况一:
typedef int DataType;
class Stack
{
public:
//构造函数
Stack(int capacity)
:_a((DataType*)malloc(sizeof(DataType)* capacity))
, _top(0)
,_capacity(capacity)
{
//我们需要_a做检查,初始化列表完成不了
if(nullptr == _a)
{
perror("Stack::malloc");
exit(-1);
}
//我们还需要_a数组初始化一下,初始化初始化列表也完成不了
memset(_a, 0, sizeof(int) * _capacity);
}
//析构函数
~Stack()
{
if (_a)
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
}
private:
DataType* _a;
int _top;
int _capacity;
};
情况二:
使用构造函数申请一个二维数组
class AA
{
public:
AA(int row, int col)
:_row(row)
,_col(col)
{
_a = (int**)malloc(sizeof(int*) * row);
for (int i = 0; i < col; ++i)
{
_a[i] = (int*)malloc(sizeof(int) * col);
}
}
private:
int** _a;
int _row;
int _col;
};
初始化列表并不能帮我们完成初始化所有的事情
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
//A.输出1 1
//B.程序崩溃
//C.编译不通过
//D.输出1 随机值
测试运行:
答案:D
1.3 explicit关键字
构造函数不仅可以初始化和赋值,对于单个参数的拷贝构造函数,还支持隐式类型转换
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a;
};
int main()
{
A a(1);
A b = 2; //隐式类型转换,从内置类型到自定义类型
return 0;
}
对于单个参数的拷贝构造函数,编译器是允许这样调用构造函数的,实际上这里还调用了一次拷贝构造函数
在语法上这里一段代码A b = 2;等价于下面两句代码:
A tmp(2); //调用构造函数
A b = tmp; //调用拷贝构造函数
先构造,在拷贝构造
早期的编译器就是这么处理的,当编译器遇到A b = 2;时它会先调用构造函数创建一个临时对象,然后讲这个临时对象拷贝构造给b。但是现在编译器已经做了优化,遇到A b = 2;时会按照A b(2);来进行处理,只会调用一次构造函数,这就是隐式类型转换
在我们仪以前的学习中,其实我们也遇到过隐式类型转换,例如:
int a = 10;
double b = a;
在这个过程中,为了保护a的值不被破环,会产生一个临时变量来存放a的值,然后将这个临时变量赋值给b
从汇编可以看到确实是这样操作的
但是,A b = 2;这种形式的可读性不是很好,所有我们可以使用explicit关键字来修饰构造函数,这样就可以禁止单参数构造函数的隐式类型转换
可以看到加入explicit之后就报错了
2.STATIC成员
2.1 概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的 成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
2.2 特性
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
例:
可以看到类中的静态变量不计入类大小,因为它存在于静态区,但是它属于类,属于这个类所有对象的成员
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
class A
{
private:
static int _a;
};
int A::_a = 10;
单独一个类对象成员定义的地方是初始化列表,但是_a是静态成员变量,属于所有A类对象的成员,不是属于某一个对象的,所有它不能使用初始化列表,也不能给初始值,所以它是一个特例,不受访问限定符的影响,在全局域初始化
3. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
例:
class A
{
public:
A(int p)
:_p(p)
{
cout << "A()" << endl;
}
static void Print()
{
cout << _p << _a << endl;
}
private:
int _p;
static int _a;
};
int A::_a = 10;
int main()
{
A a(5);
a.Print();
return 0;
}
静态成员函数里面不能使用非静态成员
tips:
含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量
4. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
(1)当静态成员变量公有时,有以下几种访问形式
class A
{
public:
int _p;
static int _a;
};
int A::_a = 10;
int main()
{
A a;
cout << a._a << endl; //使用类对象突破类域对静态成员进行访问
cout << A()._a << endl; //使用匿名结构体突破类域对静态成员进行访问
cout << A::_a << endl; //使用类名突破类域对静态成员进行访问
}
(2)当静态成员变量是私有时,有以下几种访问形式
class A
{
public:
int _p;
//没有this指针,指定类域和访问限定符就可以访问
static int GetACount()
{
return _a;
}
private:
static int _a;
};
int A::_a = 10;
int main()
{
A a;
cout << a.GetACount() << endl; //通过对象调用静态成员函数来进行访问
cout << A().GetACount() << endl; //通过匿名对象调用静态成员函数来进行访问
cout << A::GetACount() << endl; //通过类名调用静态成员函数进行访问
return 0;
}
这里说明一下:
使用非静态成员函数只能 得到_a的值,来使用它的值,并不能访问_a
class A
{
public:
int _p;
int GetACount()
{
return _a;
}
private:
static int _a;
};
int A::_a = 10;
int main()
{
A a;
cout << a.GetACount() << endl; //通过对象调用静态成员函数来得到_a的值
return 0;
}
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
当静态成员是private时,尽管突破了类域也不能对其进行访问
【问题】
1. 静态成员函数可以调用非静态成员函数吗?
不可以,因为非静态成员函数第一个形参是this指针,而静态成员函数中没有this指针,所以静态函数成员不能调用非静态函数
2. 非静态成员函数可以调用类的静态成员函数吗?
可以,因为在类域中静态成员函数不受类域和访问限定符的限制
使用static情景:
设计一个类,只能在栈上创建对象
class A
{
public:
static A GetStackObj()
{
A N1;
return N1;
}
private:
A()
{}
private:
int _a;
int _b;
};
int main()
{
A::GetStackObj();
return 0;
}
3.C++中成员初始化的新玩法
C++11支持非静态成员变量在声明时进行初始化,但是要注意这里不是初始化,而是给初始化列表一个缺省值
class A
{
public:
A()
{}
private:
//非静态成员声明时可以给缺省值
int* _a = (int*)malloc(sizeof(int) * 10);
int _b = 10;
static int _c; //非静态对象不能直接给缺省值
};
初始化列表是你初始化定义的地方,如果你没有给值,那它就会使用你给的缺省值,如果你给了值,那么它就不会使用你的缺省值
4.友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度(就是双方之间的亲密度),破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
4.1 友元函数
问题:现在尝试去重载operator<<,然后发现没办法将operator重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator>>重载成成员函数但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
我们知道在C++有一件很神奇的事,就是cout和cin可以自动识别输入和输出对象的类型,我们使用它们时不用像C语言一样增加输入输出的格式,这给我们提供了便利,难道C++真有这么神奇吗?其实不是,内置类型的变量可以直接使用cout和cin,是因为它们在库中已经实现了内置类型于运算符的重载
可以查看到,cout在ostream这个类中,而cin在istream这个类中
我们来实现一下自定义运算符的重载,让自定义类型也可以使用<<和>>
这里友元函数就起到作用了
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>> (istream& _cin, Date& d);
public:
Date(int year = 1, 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 >> d._month >> d._day;
return _cin;
}
int main()
{
Date d1;
cin >> d1;
cout << d1;
return 0;
}
这里上一节更详细大家有兴趣的可以去看看这里是链接 点击这里
注:
cout是ostream类中的一个全局对象,cin是istream类中的一个全局对象,<<和>>重载具有返回值,是为了连续的实现<<和>>的连续运算,它们的生命周期都是整个工程
说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
4.2 友元类
声明友元类之后,Date中你所有成员函数都是Time中的友元函数,即:Date中的所有成员函数都可以访问Time中的私有成员
//友元类
class Time
{
//声明Date是Time的友元类
friend class Date;
public:
Time(int hour = 1, int minute = 1, int second = 1)
:_hour(hour)
,_minute(minute)
,_second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
//使用Time类
void SetTimeOFdate(int hour = 1, int minute = 1, int second = 1)
{
T._hour = hour;
T._minute = minute;
T._second = second;
}
private:
int _year;
int _month;
int _day;
Time T;
};
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
- 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递 如果B是A的友元,C是B的友元,则不能说明C时A的友元。
- 友元关系不能继承,在继承位置再给大家详细介绍。
5.内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中 的所有成员。但是外部类不是内部类的友元。
特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
static int _a;
int _b;
//B是A的内部类,B类是A类的天生友元
class B
{
private:
void foo(A& a)
{
cout << a._b << endl;
cout << _a << endl;
}
};
};
计算大小为:4
这里A的大小为4,内部类不算做整体的大小
6.再次理解封装
C++是一门面向对象的语言,面向对象编程语言具有三个特征:封装,继承,多态
C++通过类,将对象的属性和行为封装在一起,就像去旅游一样,去参观一个景点,需要先预约买票,然后排队进入,进去之后只供观赏。若兵马俑可以让人随意的触摸,你一下我一下这些文物能保留多久呢?
所以我们去参观兵马俑时,他只允许我们观赏和拍照,并不允许我们做其他操作,这就是属于封装,通过将景点封装起来提供一个行为供我们使用。防止物品不让其他人破坏
博物馆管理系统
售票系统:负责将票卖给游客
工作人员: 检票,打扫,服务,安保,卫生等
导游:带领客户到指定位置参观博物馆,并且给客户讲解物品的缘由,历史等
通过对比理解,其实C++也是一样,将对象的属性和行为封装在一个类中,通过访问限定符,对类中的内容来进行管理,将用户需要的,我们能提供的设置为:public,为用户提供一个接口,至于其中实现的原理,用户就不需要知道,知道反而会增加使用或者维护的难度
7.再次理解面向对象
其实面向对象就是模拟抽象现实世界
标签:初始化,对象,成员,C++,int,静态,class,构造函数 From: https://blog.csdn.net/m0_73634434/article/details/142188162