构造函数-2
构造函数体赋值
在对象创建的时候,编译器会调用构造函数,给对象当中的成员赋一个合适的初始值。
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)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
如上图所示,数据成员后面的括号当中就是给这个数据成员初始化值。
看似这个初始化和 赋初值,看似差不多,但是在有些情况下,初始化列表才能解决。
【注意】:
- 每一个成员最多只能再初始化链表当中出现一次。
- 有些成员必须在初始化列表初始化
上述必须初始化的成员有 :
- 引用成员变量
- const成员变量
- 自定义成员类型(当其类中没有默认构造函数)
比如我们直接使用构造中 直接赋初值,来初始化这个 const 的成员:
像上述就报了,必须初始化常量限定····这个错,这就代表着要在 定义的时候初始化,那么我们在赋值的时候是不行的。
那么我们就理解实质了,也就是说,如我们不使用初始化列表,那么我们在构造函数中的初始化就是直接 赋初值,那么这种方式是不能初始化 在定义的时候就需要初始化的成员的。
也就是说,在C++当中,这个位置不认为是初始化的地方:
如上图,这个只是赋值。
在这个位置才认为是 初始化。
我们之前说过,当我们初始化一个成员的时候,如果是内置类型,不进行处理:
但是也不是都不处理,如果这个内置类型,在类当中是定义给了值的:
这个不是初始值,这个是 给 初始化列表的缺省值,也就是说,如果我们不对这个内置类型进行处理,那么这个x 就会被 赋 1 (缺省值)这个值,如果我们在初始化列表当中对这个x 进行了赋值,那么这个x 就是 在初始化列表当中初始化的值。
同样,如果对象是 没有默认构造函数,那么我们在创建这个对象的时候,就会报错:
如上图,我们在B类当中创建一个了一个 A类的对象,但是如下图所示,(没有缺省参数)而我们创建的是无参的构造函数来创建这个对象的,我们构造函数当中没有默认构造函数,那么就会报错:
当我们在A 的构造函数中 的 初始化列表当中去的 调用这个对象的构造函数,去创建这个对象,那么就不会报错:
而且我们发现,成功赋值。
所以我们建议我们能使用初始化列表,就用初始化列表,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表来进行初始化,因为初始化列表是这个些成员定义的地方,但是也不是意味着所有的初始化都可以用初始化列表。
class Stack
{
public:
Stack(int Top, int capacity)
:_a((int*)malloc(sizeof(int)))
,Top(Top)
,capacity(capacity)
{
if (_a == NULL)
{
perror("malloc fail");
exit(-1);
}
// 初始化数组
memset(_a, 0, sizeof(int) * capacity);
}
protected:
int* _a;
int Top;
int capacity;
};
比如上述例子,我们在实现栈的构造函数的时候,我们希望在 初始化列表当中去 初始化 _a 指针指向一块空间。那么我们是可以实现的。
但是,我们知道,malloc函数是有可能会开辟空间失败的,所以我们要进行判断,但是这个判断在是在 初始化列表当中不能做到的,我们就只能再的 构造函数中实现,那既然有这样的场景,那么假设我们还想初始化我们创建的数组,那么我们还需要再使用这个函数来进行处理,如上述例子一样。
这些都是初始化列表不能实现的,而且,这种情况很多时候不止一种,可能会有很多行,我们这里想表达的意思是,总有一些工作时初始化列表做不完的。那么我们就可以在函数体当中去实现。
再比如我们要动态开辟二维数组,那其中必然就有一个循环,这也是初始化列表不能实现的。
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后
次序无关。
如下例子:
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
上述例子,输出的不是 1 1 ,而是 1 随机值。
因为上述例子,我们是声明的 _a2 这个变量,那他就会先初始化,那么我们在初始化列表当中写的是 _a2(_a1) 此时 _a1 还没有会被初始化,所以是随机值。先走 _a2(_a1) 再走 _a1(a)。
上述例子还不是很严重,我们在之前实现栈的构造函数的时候,如果想下述这样写就会出错了:
我们发现上述在初始化 _a 的时候,capacity是没有初始化的,也就是说现在 的 capacity 是一个随机值,那么我们以一个随机值来开辟空间大小,就会出现很严重的问题。
所以,我们一般把声明的顺序和初始化的顺序保持一致。
explicit关键字
隐式类型转换
例子:
B b(10); //1
B c = 10; //2
代码1是在调用 B 这个类的构造函数来创建这个对象,那么 代码2是在干嘛呢?
我们发现他是一个隐式的类型转换,由整形类型转化成自定义类型。
他是先用 10 ,利用 B 的构造函数来创建一个 临时对象,这个临时对象的类型就是 自定义类型B,然后 c 再用 B 当中的拷贝构造函数去 把 临时对象,拷贝给 c 这个对象。
我们发现,上述即调用了 构造函数,还调用了 拷贝构造函数,但是编译器不想这样做,他不像上面这个代码,即调用构造函数有调用拷贝构造函数,所以,一遍像上述情况,编译器会进行优化。
像上述例子,他会直接使用 10 这个整型数据来进行 构造。
我们把 B的拷贝构造写出来,其中加入打印,看看构造函数和 拷贝构造函数是否被调用:
class B
{
public:
B(int n)
:_n(n)
//,aobj(10)
{
cout << "B::B(int)" << endl;
}
B(const B& b)
:_n(b._n)
{
cout << "B::B(const B& b)" << endl;
}
protected:
const int _n;
};
int main()
{
B b(10); //1
B c = 10; //2
return 0;
}
输出:
我们发现,拷贝构造函数并没有被调用,此处就是被编译器优化了,如果按照本来的实现过程,应该还有临时对象的创建。
构造函数和 拷贝构造函数都是 构造函数,除了老的编译器,现在的编译器一般都不会容忍在同一个表达式当中重复的调用构造函数。
我们现在来举一个反例,来验证我们刚刚说个,创建临时对象这一过程:
B& pb1 = 2; //代码1
pb1 去引用之前的 b 和 c 都是可以的 ,但是,我们上述引用的是一个整形 2 ,这就不行了,报错:
但是如果我们把这个引用转换成 const 的就可以了:
const B& pb2 = 2; //代码2
我们发现上述代码编译通过了。
我们上述定义的是 B 类型的引用类型,编译器在这时候就不能在进行优化了,上述代码的实现过程就是我们之前说的,创建一个 临时对象来进行 赋值,像上述的代码2,就是用2 ,调用构造函数创建了一个 临时对象,这里的 pb2 引用的就是 这个临时对象。
而临时对象具有常性,所以,之前的报错是 也 我们的引用类型不是 const 所修饰的引用,当我们用const 修饰之后就可以编译通过了。如下图所示:
此时我们运行这个代码,输出:
我们发现只调用了一次构造函数。
那么像上述的这种创建一个临时对象来让一个自定义类型,接收一个不是这个类型的值,然后来创建这个对象,主要是为了实现像下述代码这种情况:
class list
{
public:
list(const string& st)
{
}
};
int main()
{
string name1("李四");
list LS1(name1); // 代码1
list LS2("李四"); // 代码2
return 0;
}
上述代码 1 和 代码 2 ,肯定是 代码2 的方式比较方便,代码能实现这种情况的是因为,“李四” 这个字符串就和 const String& 之间就发生了 隐式类型转换,和之前的 const B& pb2 = 2; 是一样的。
当我们 不加 const修饰就报错了:
explicit关键字
根据上述的 list 类的描述,我们知道:构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用
那么我们在自定义类型,实现构造函数的时候,我们就可以在函数名之前加一个 explicit 这个关键字,来防止类似于 B b = 10 这样的 隐式类型转换:
Static 成员
假设我们现在想要,记录当前我们实现的类,到底创建了多少个对象,那么我们可以在 全局当中定义一个变量(scount),用这个变量来计算我们创建了多少个 对象;当我们调用这个类当中的 构造函数或者是拷贝构造函数的时候,我们就 scount++;如果我们调用的是 析构函数就 scount--。从而来记录,当前有多少个这个类的对象。
static int _scount = 0;
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
};
void func(A aa)
{
cout << __LINE__ << " : " << _scount << endl;
return;
}
int main()
{
cout << __LINE__ << " : " << _scount << endl;
A aa1;
A aa2;
func(aa2);
cout << __LINE__ << " : " << _scount << endl;
}
输出:
我们上述使用 __LINE__ 这个宏来打印当前行数,上述就输出了 每一行代码的情况下当前有多少个对象。
当然,不管你在 局部,全局,还是在 对象当中去创建这个对象,只要是创建对象都需要构造函数或者是 拷贝构造函数,那么只要调用,计数器就会++;只要调用析构函数,计数器都会 --。
如上述,我们在函数中创建的 对象,在对象当中打印这个 计数器,发现是3,说明当前已经创建这个局部的对象,但是在函数调用结束之后,对象会调用析构函数进行销毁,那么计数器就会--,所以我们在主函数当中打印的计数器的值是 2。
但是,我们上述使用的全局变量,这样做不太好,所以我们在定义在定义类型的时候,可以对类似这样需要全局实现的 变量,进行封装,把这边变量封装到 自定义类型当中。
Static 成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用
static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
这里我们要搞清楚,普通成员变量和 静态成员变量,这两个的区别,普通成员属于类的当前对象,而 静态成员变量属于的是类的每一个对象,因为是静态的,他的生命周期是全局的;而普通成员的生命周期是 这个对象的生命周期。
因为 静态成员变量是属于全局的,所以我们在 对象当中,利用构造函数去初始化是不行的:
如上图,报错了。
静态成员变量只能再 全局位置 ,类外面进行 定义。像下面这样定义这个静态成员变量。
int A::_scount = 0;
那么像我们之前实现的 _scount 是 protected 的,那么我们就不能直接 A::_scount 这样访问了,除非这个 _scount 是 public 的。
当然为了封装,我们一般不会是 共有的,一般是私有的,public的和 全局的每什么区别,达不到封装的效果。
所以为了取到 不是 public的 静态成员变量,我们就创建一个 静态成员函数,来取到 其中的 静态成员变量。
静态成员函数没有隐藏的this指针,不能访问任何非静态成员
static int Get_Scount()
{
return _scount;
}
因为这个静态成员函数不能访问非静态的成员,他能帮我们取到 静态的成员的值,所以我们使用这个函数来取到数据,又不会破坏封装性。
class A
{
public:
A()
{
++_scount;
}
A(const A& t) { ++_scount; }
~A() { --_scount; }
static int Get_Scount()
{
return _scount;
}
protected:
static int _scount;
};
int A::_scount = 0;
void func(A aa)
{
cout << __LINE__ << " : " << A::Get_Scount() << endl;
return;
}
int main()
{
cout << __LINE__ << " : " << A::Get_Scount() << endl;
A aa1;
A aa2;
func(aa2);
cout << __LINE__ << " : " << A::Get_Scount() << endl;
}
我们可以通过 " . " 或者是 " 类名:: " 来访问静态成员函数。
静态成员变量是不能给缺省值的,因为静态成员变量的初始化不在 初始化列表当中,而是在 全局当中。
关于静态和非静态的关系:
同类中 非静态可以调用非静态,而静态 只要是不受指定类域,和访问限定符的限定就可以访问。
因为 访问非静态的成员函数的访问,需要 this 指针,而在静态成员函数中没有 this 指针。
设计一个类只能在 栈/堆上创建对象
如下,在C++当中的对象存储的位置:
class A
{
public:
protected:
int _a = 1;
int _b = 1;
};
int main()
{
static A aa1; // 静态区
A aa2; // 栈
A* ptr = new A; // 堆
return 0;
}
我们发现,我们创建对象的地方有好几个,而且像上述一样实现的类,用户可以在 静态区 栈 堆 上随便选个位置来创建对象。
我们可以把 构造函数 的访问权限 设置为 protected 私有的 ,这样我们在类外部就不能随便去调用这个构造函数,去创建对象。
那么问题来了,既然是私有的,那么我们在类外部怎样去 调用这个 构造函数 呢?
我们可以定义 public 的 成员函数,既然在来外面不能调用,那么我们就在 类当中去调用构造函数,如下所示:
这样我们可以通过调用这些函数 来在对应的 位置创建对象。
但是,向上述还是有问题,既然这个函数是成员函数,那么我们如果在类外面访问这个函数呢?
我们如果想访问成员函数必须通过对象来访问,但是现在,我们不能创建对象。
这时候,静态的成员函数就可以帮我们解决这个问题,因为静态的成员函数就可以在类外面进行调用,他是全局的,不在类当中。
class A
{
public:
static A GetStackA()
{
A aa;
return aa;
}
static A* GetHeapA()
{
return new A;
}
protected:
int _a = 1;
int _b = 1;
};
如上述这种,我们就可以只创建对应的函数,来达到只能再某一个区域创建对象的这种限制。
例题:
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
这里就可以利用这个 静态成员,通过调用n次构造函数来,计算出从1 加到 n 的值。这里我们想到创建一个这个类的数组,数组元素个数为 n。
class Sum
{
public:
Sum()
{
_sum += _i;
_i++;
}
int GetSum()
{
return _sum;
}
protected:
static int _sum;
static int _i;
};
int Sum::_sum = 0;
int Sum::_i = 1;
class Solution {
public:
int Sum_Solution(int n) {
Sum a[n];
return a[0].GetSum();
}
};