一. 再谈构造函数
构造函数里,类初始化成对象有2种方式:构造函数体赋值、初始化列表
之前学的是构造函数体赋值:
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、自定义类型成员(且该类没有默认构造函数时)
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
为什么设计初始化列表?
引用、const 成员必须在定义的时候初始化:
引用:必须变成谁的别名
const:只有1次初始化的机会(定义的时候)
什么是定义的地方?
对象实例化时整体定义。调用构造函数,对每个成员初始化
对象里有多个成员变量,每个成员变量在初始化列表定义
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
private:
int _a;
};
class C
{
public:
C(int c)
:_c(c)
{
cout << "C(int c)" << endl;
}
private:
int _c;
};
class B
{
public:
// B(int a, int ref) 引用的局部变量,出作用域销毁
B(int a, int& ref) // ref是n的别名
:_ref(ref) // _ref是ref(n)的别名
,_n(1)
,_z(9) // 显示给,不用缺省值
,_ck(10) // 没有默认构造,必须在这里显示调用构造
,_ak(99) // 有默认构造也可以显示调(缺省参数)
{}
private:
A _aobj; // 有默认构造函数
A _ak;
C _ck; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
int _x;
int _y = 1; // 1是缺省值,是给初始化列表的
int _z = 1;
};
int main()
{
// B bb(10, 1);
int n = 1;
B bb(10, n);
return 0;
}
我们不写,内置类型不处理;自定义类型调用(在初始化列表调用)默认构造函数
typedef int DateType;
class Stack
{
public:
Stack(int capacity = 4) // 构造函数,功能:替代Init
{
_a = (DateType*)malloc(sizeof(DateType) * capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DateType Date)
{
CheckCapacity();
_a[_size] = Date;
_size++;
}
void Pop()
{
if (Empty())
return;
_size--;
}
DateType Top() { return _a[_size - 1]; }
int Empty() { return 0 == _size; }
int Size() { return _size; }
~Stack()
{
cout << "~Stack()" << endl;
if (_a)
{
free(_a);
_a = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
void CheckCapacity()
{
if (_size == _capacity)
{
int newcapacity = _capacity * 2;
DateType* temp = (DateType*)realloc(_a, newcapacity *
sizeof(DateType));
if (temp == nullptr)
{
perror("realloc申请空间失败!!!");
return;
}
_a = temp;
_capacity = newcapacity;
}
}
private:
DateType* _a;
int _capacity;
int _size;
};
class MyQueue
{
public:
MyQueue() // 不传参就这样写
{ }
MyQueue(int capacity) // 自己控制capacity,手动传参
:_pushst(capacity)
,_popst(capacity)
{ }
private:
Stack _pushst;
Stack _popst;
};
int main()
{
MyQueue q1; // 调用无参的构造函数
MyQueue q2(100); // 调用带参的构造函数(构造函数可以构成重载)
return 0;
}
都会对 _pushst _popst 初始化,所有成员都会走且只能走1次初始化列表。因为那是它定义的地方
写了就用你的,不写也会走初始化列表
自定义类型必须调用构造函数,引用、const 必须在定义的时候初始化
有默认构造函数:我就调默认构造函数,不传参也可以
没有默认构造函数(eg:只有一个带参的构造函数):在初始化列表时就不知道怎么初始化成员了所以:没有默认构造函数,编译器不知道怎么初始化成员,就报错了,他要求你来
初始化列表并不能解决所有问题
eg1:要求 检查、初始化数组
class Stack
{
public:
Stack(int capacity = 10)
: _a((int*)malloc(capacity * sizeof(int)))
,_top(0)
,_capacity(capacity)
{
if (nullptr == _a)
{
perror("malloc申请空间失败");
exit(-1);
}
// 要求数组初始化一下
memset(_a, 0, sizeof(int) * capacity);
}
private:
int* _a;
int _top;
int _capacity;
};
eg2:动态开辟二维数组
class AA
{
public:
AA(int row = 10, int col = 5)
:_row(row)
,_col(col)
{
_aa = (int**)malloc(sizeof(int*) * row);
for (int i = 0; i < row; i++)
{
_aa[i] = (int*)malloc(sizeof(int) * col);
}
}
private:
int** _aa;
int _row;
int _col;
};
3. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
声明、定义的顺序要保持一致
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1) // 先走这个
{}
void Print() {
cout << _a1 << " " << _a2 << endl; // 1 随机值
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
explicit关键字
类型转换中间会产生临时变量
int i = 10;
double d = i;
中间会产生 double 类型的临时变量,临时变量再给 d
临时变量又有常性,所以 double& d = i; 错误 const double& d = i; 正确
class A
{
public:
A(int a)
:_a(a)
{ }
private:
int _a;
};
int main()
{
A aa1(1); // 调构造
A aa2 = 2; // 隐式类型转换,整型转化为自定义类型
return 0;
}
用 2 调用构造函数,生成 A 类型的临时对象;临时对象再拷贝构造 aa2(构造+拷贝构造)
在同一个表达式内,连续构造会被优化:用 2 直接构造
验证:用 2 直接构造
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 aa2 = 2; // 隐式类型转换,整型转化为自定义类型
return 0;
}
验证:用 2 调用构造函数,生成 A 类型的临时对象;临时对象再拷贝构造 aa2(构造+拷贝构造)
int main()
{
A& aa3 = 2; // aa3 引用 aa2 肯定没毛病,但不能引用 2
// error C2440: “初始化”: 无法从“int”转换为“A &”
const A& aa3 = 2; //
return 0;
}
引用不可以;const引用 可以,还调了一次构造:
这个玩法的用处:
#include <string>
//#include <list>
//class string
//{
//public:
// string(const char* str) // string类中的一个构造函数,可以用一个字符串去构造string类
// {}
//};
class list
{
public:
void push_back(const string& str)
{}
};
int main()
{
string name1("张三"); // 构造
string name2 = "张三"; // 构造+拷贝构造+优化
// 结果一样,但过程不一样
list lt1;
string name3("李四");
lt1.push_back(name3); // 老老实实构造,你是string,我传string给你
lt1.push_back("李四"); // 更舒服
return 0;
}
支持 28行写法的原因:隐式类型转换
"李四"是 const char*,会去调它的构造(string(const char* str)),构造一个临时对象;
临时对象有常性,符合void push_back(const string& str)
如果不想让转换发生呢?
class A
{
public:
explicit 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 aa2 = 2; // error C2440: “初始化”: 无法从“int”转换为“A”
const A& aa3 = 2; // error C2440: “初始化”: 无法从“int”转换为“const A &”
return 0;
}
智能指针就不想发生转换
二. static 成员
想统计 A 创建了多少个对象
先想到定义全局变量
int _scount = 0;
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
// static int GetACount() { return _scount; }
private:
// static int _scount;
};
A aa0;
A Func(A aa1)
{
cout << __LINE__ << ":" << _scount << endl;
return aa;
}
int main()
{
cout << __LINE__ << ":" << _scount << endl;
A aa1;
static A aa2;
Func(aa1);
cout << __LINE__ << ":" << _scount << endl;
return 0;
}
输出结果:24:1 18:4 28:3
24行的那一个对象是 aa0
说明:全局对象在 main 函数之前就会调用构造;局部的静态对象不会在 main 函数之前初始化
到 18 行,有 aa0 aa1 aa2,还有一个是自定义类型传参调用的拷贝构造
传值返回,27行结束就销毁了
A aa0;
void Func()
{
static A aa2;
cout << __LINE__ << ":" << _scount << endl;
}
int main()
{
cout << __LINE__ << ":" << _scount << endl; // 1
A aa1;
Func(); // 3
Func(); // 3
return 0;
}
aa2 是局部的静态对象,不在函数栈帧里,在静态区,只会定义 1 次
祖师爷不喜欢这种方式,不想让你随便访问,提出封装的方式
2.1 概念
声明为 static 的类成员称为类的静态成员。用 static 修饰的成员变量,称之为静态成员变量;用 static 修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
2.2 特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
- 静态成员变量不能给缺省值。缺省值是给初始化列表用的,它没有初始化列表
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
// 3.没有this指针,指定类域和访问限定符就可以访问
static int GetACount()
{
// _a1++; 错! 5.静态里,不能访问非静态的,因为没有this指针
return _scount;
}
private:
// 成员变量 -- 属于每个一个类对象,存储对象里面
int _a1 = 1;
int _a2 = 2;
// 静态成员变量 -- 1.属于类,属于类的每个对象共享,存储在静态区(生命周期是全局的)
static int _scount;
};
// 2.全局位置,类外面定义。不能在初始化列表定义,因为它不是对象自己的成员
int A::_scount = 0;
A aa0;
void Func()
{
static A aa2;
cout << __LINE__ << ":" << aa2.GetACount() << endl; // 3.对象.静态成员
}
int main()
{
cout <<__LINE__<<":"<< A::GetACount() << endl; // 3.类名::静态成员
A aa1;
Func();
Func();
return 0;
}
因为是私有,上面就不能直接访问 _scount,只能通过公有的成员函数
静态成员变量和静态成员函数一般都是配套出现
问题:
1. 静态成员函数 可以调用 非静态成员函数 吗?
2. 非静态成员函数 可以调用 类的静态成员函数 吗?
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
void Func1()
{
// 非静态能否调用静态:可以:没有类域、限定符限制
GetACount();
}
void Func2()
{
++_a1;
}
static int GetACount()
{
// 静态能否调用非静态:不可以:非静态的成员函数调用需要this指针,我没有this
Func2();
// _a1++; 静态里,不能访问非静态的,因为没有this指针
return _scount;
}
private:
// 成员变量
int _a1 = 1;
int _a2 = 2;
// 静态成员变量
static int _scount;
};
一个用 static 的绝佳场景(一种思想):
设计一个类,在类外面只能在栈上创建对象
设计一个类,在类外面只能在堆上创建对象
class A
{
public:
static A GetStackObj()
{
A aa;
return aa;
}
static A* GetHeapObj()
{
return new A;
}
private:
A()
{}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
//static A aa1; // 静态区
//A aa2; // 栈
//A* ptr = new A; // 堆
A::GetStackObj();
A::GetHeapObj();
return 0;
}
调用这个成员函数需要对象,但这个函数是为了获取对象(先有鸡还是蛋的问题)用 static 可以解决
三. 友元
友元提供了一种 突破封装 的方式,提供了便利。但会增加耦合度,破坏了封装,所以友元不宜多用
友元分为:友元函数 和 友元类
1. 友元函数
我的函数声明成你的朋友,我在类外面就可以访问你的私有
说明:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰:友元函数没有this指针
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制:访问限定符限制的是成员(成员变量、成员函数)的访问方式。这只是个友元声明
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
在类外面有时需要用对象访问成员。
以前访问的方式是成员函数,成员函数的第一个位置都是this指针
下面的代码要符合用法,流对象 ostream的cout对象 和 istream的cin对象 要抢占左操作数。写成成员函数会抢位置,所以不能写成成员函数(详解见上一篇文章的 流插入打印、流提取 )【C++】类和对象(中)拷贝构造、赋值重载_c++构造值一样的对象-CSDN博客
Date.h
class Date
{
// 友元函数声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1); // 构造
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
Date.cpp
Date::Date(int year, int month, int day) // 构造
{
if (month > 0 && month < 13
&& day > 0 && day <= GetMonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
int year, month, day;
in >> year >> month >> day;
if (month > 0 && month < 13
&& day > 0 && day <= d.GetMonthDay(year, month))
{
d._year = year;
d._month = month;
d._day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
return in;
}
友元提供了便利,增加了耦合(关联度)
eg:不想叫_year,想叫year,类里面得改,友元函数里也得改
2. 友元类
我的类成为你的友元,在我整个类里面,可以随便访问你的私有、保护
说明:
- 友元关系是单向的,不具有交换性
比如上述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;
};
四. 内部类
概念:如果一个类定义在另一个类的内部,这个内部的类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
- 受访问限定符的限制
定义出来给别人用的东西(目前学的:成员变量,成员函数,内部类)才会收到访问限定符的限制
class A
{
private:
static int k;
int h;
public:
class B
{
public:
void foo()
{ }
private:
int b;
};
};
int A::k = 1;
int main()
{
cout << sizeof(A) << endl; // 4
A aa;
A::B bb; // 如果 B是私有的,这样就错
return 0;
}
k 没有存在对象里,所以不计算 k 的大小
A类 里面没有创建 B对象,所以不算 b 的大小
class A
{
private:
static int k;
int h;
public:
class B
{
public:
void foo()
{ }
private:
int b;
};
B _bb; // A类 里,用B类 创建了对象 _bb。要算 _bb的大小
};
int A::k = 1;
int main()
{
cout << sizeof(A) << endl; // 8
return 0;
}
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元,内部类是外部类的天生友元
{
public:
void foo(const A& a)
{
cout << k << endl; // OK
cout << a.h << endl; // OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
五. 匿名对象
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution
{
public:
int Sum_Solution(int n)
{
cout << "Sum_Solution" << endl;
//...
return n;
}
};
int main()
{
A aa(1); // 有名对象 -- 生命周期在当前函数局部域
A(2); // 匿名对象 -- 生命周期在当前行
// 后面没人用,干脆直接销毁
// 想调用Sum_Solution函数
// 1.有名对象
Solution sl;
// 不能加():Solution sl(); // 不知道是对象还是函数名
sl.Sum_Solution(10);
// 2.匿名对象
Solution().Sum_Solution(20);
// 必须加()
// Solution.Sum_Solution(20); // 错,必须是 "对象." 要传this
// Solution::Sum_Solution(20); // 错,只有静态成员函数才能这么调,因为没有this指针
// A& ra = A(1); // 错,匿名对象具有常性
const A& ra = A(1); // const引用延长匿名对象的生命周期,生命周期在当前函数局部域
// ra还要用,留下来
Solution().Sum_Solution(20);
return 0;
}
void push_back(const string& s) // 如果不加const,仅第一个可以编过
{
cout << "push_back:" << s << endl;
}
int main()
{
string str("11111"); // 有名对象
push_back(str);
push_back(string("222222")); // 匿名对象。传上去const引用,延长生命周期
push_back("222222"); // 临时对象。隐式类型转换(详解见上文explicit关键字)
return 0;
}
六. 拷贝对象时的一些编译器优化
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void Func1(A aa)
{}
void Func2(const A& aa)
{}
int main()
{
A a1;
Func1(a1); // 传值传参:a1传给aa,要调用拷贝构造
Func2(a1); // 没有拷贝构造
return 0;
}
void Func1(A aa)
{}
void Func1(const A& aa) // 类型不一样,构成重载
{}
int main()
{
A a1;
Func1(a1); // “Func1”: 对重载函数的调用不明确 <==> 无参、全缺省
return 0;
}
A Func3()
{
A aa;
return aa;
}
int main()
{
Func3();
return 0;
}
旧编译器:传值返回,返回aa的拷贝
新编译器:连续的构造+拷贝 ==> 直接构造
A& Func4() // 只有静态,出了作用域没有销毁(*this)才能用引用返回
{
static A aa;
return aa;
}
int main()
{
Func4();
return 0;
}
传引用返回,没有拷贝
A Func5()
{
A aa;
return aa;
}
int main()
{
Func5(); // 这个表达式返回的值是aa拷贝的临时对象
// 所以不能这样接收:A& ra = Func5();
const A& ra = Func5();
return 0;
}
A Func5()
{
A aa;
return aa;
}
int main()
{
A ra = Func5();
return 0;
}
同一行连续的一个步骤里,>=2个 构造\拷贝构造\构造+拷贝构造 有可能优化
void Func1(A aa)
{}
A Func5()
{
A aa;
return aa;
}
int main()
{
A ra = Func5(); // 拷贝构造+拷贝构造 --> 优化为拷贝构造
A aa1;
Func1(aa1); // 不优化。在2行
Func1(A(1)); // 构造+拷贝构造 --> 优化为构造
Func1(1); // 构造+拷贝构造 --> 优化为构造
A aa2 = 1; // 构造+拷贝构造 --> 优化为构造
// 19.20 行等价
return 0;
}
int main()
{
A ra1 = Func5(); // 拷贝构造+拷贝构造 --> 优化为拷贝构造
cout << "==============" << endl;
A ra2;
ra2 = Func5(); // 不会优化。对象已经定义出来了;而且这里不是拷贝构造,是赋值
return 0;
}
构造、拷贝构造尽量写到1个步骤里
本篇的分享就到这里了,感谢观看,如果对你有帮助,别忘了点赞+收藏+关注。
小编会以自己学习过程中遇到的问题为素材,持续为您推送文章