一.再谈构造函数
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;
};
对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体 内可以多次赋值。
1.2初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括 号中的初始值或表达式
这里我们来思考一个问题 代码如下:
class stack
{
stack()
{
}
};
class MYQUEUE
{
public:
//stack类中如果不具备默认构造,myqueue也无法生成默认构造
MYQUEUE(int n)
{
}
private:
stack _pushst;
stack _popst;
int _size;
};
在这个代码中 myqueue类中的成员变量 有_pushst 和_popst两个变量是通过stack变量来定义的 那么在构造时 但是stack这个类并不具备默认构造函数 这时该如何进行构造_pushst 和_popst?
这时我们唯一可以显示使用就是初始化列表来对_pushst 和_popst进行初始化
class stack
{
public:
stack(int n)
{
}
private:
};
class MYQUEUE
{
public:
//stack类中如果不具备默认构造,myqueue也无法生成默认构造
MYQUEUE(int n)
:_pushst(n)
,_popst(n)
,_size(0)
{
}
private:
stack _pushst;
stack _popst;
int _size;
};
在这个代码中 就使用了初始化列表解决了问题
【注意】
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化: 引用成员变量
const成员变量 自定义类型成员(且该类没有默认构造函数时)
在这里的初始化列表本质就相当于每个对象中成员定义的地方 而我们的const修饰的常变量是不可改变的 而引用的变量是需要进行初始化的 且const变量只有一次初始化的机会 ,就是在定义的时候,所以const常变量和引用变量初始化都是必须通过初始化列表的
总结所有的成员都可以在初始化列表或者函数体内进行初始化 但有三类 必须在初始化列表中进行初始化 1.const变量 2. 引用变量 3.没有默认构造的自定义类型
class stack
{
public:
stack(int n)
{
}
private:
};
class MYQUEUE
{
public:
//stack类中如果不具备默认构造,myqueue也无法生成默认构造
MYQUEUE(int n,int &rr)
:_pushst(n)
, _popst(n)
,_x(0)
,_ref(rr)
{
_size = 0;
}
private:
stack _pushst;
stack _popst;
int _size;
const int _x;
int& _ref;
};
int main()
{
int xx = 0;
const int y = 1;
MYQUEUE q1(10,xx);
MYQUEUE q2=q1;
int& ry = xx;
return 0;
}
理解:初始化列表的本质可以理解为每个对象中成员定义的地方
1.3 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使 用初始化列表初始化。
1.4成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
1.5 每个初始化列表 不管你走不走 每个成员变量都会先走一遍
自定义类型的成员会调用默认构造 而内置类型会自动调用缺省值 如果没有声明缺省值 则会看编译器 有的编译器就会处理 有的编译器不进行处理
typedef int datatype;
class Stack
{
public:
Stack(size_t capacity = 3)
{
cout << "Stack()" << endl;
_array = (datatype*)malloc(sizeof(datatype)*capacity);
if (_array == 0)
{
perror("malloc fail");
return;
}
_capacity = capacity;
_size = 0;
}
void push(datatype data)
{
_array[_size] = data;
_size++;
}
~Stack()//析构函数
{
cout << "~stack" << endl;
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
datatype* _array;
int _capacity;
int _size;
};
class MYQUEUE
{
public:
//stack类中如果不具备默认构造,myqueue也无法生成默认构造
MYQUEUE()
{
}
private:
Stack _pushst;
Stack _popst;
int _size = 0;
};
int main()
{
MYQUEUE q;
return 0;
}
如下图 如果我们只写了一个_size的初始化 那么这时就会走初始化列表 而不在去访问缺省值
且在初始化列表这一块是比较自由的 可以写n+4 或者malloc之类的初始化 且顺序是先初始化列表再走函数体
在实践中 一般是先走初始化列表 当走初始化列表感到麻烦时再走函数体
这时为了将_ptr中的空间都初始化为1 走函数体使用memset是更加方便的
class MYQUEUE
{
public:
//stack类中如果不具备默认构造,myqueue也无法生成默认构造
MYQUEUE()
:_size(1)
,_ptr((int*)malloc(40))
{
memset(_ptr,1,40);
}
private:
/*Stack _pushst;
Stack _popst;*/
int _size = 0;
int* _ptr;
};
int main()
{
MYQUEUE q;
return 0;
}
这道题是选择D的 初始化列表的顺序并不代表初始化的先后顺序 代表初始化先后顺序的是声明时的顺序 在这段代码中 先声明_a2后声明_a1 所以先初始_a2为随机值 之后初始化_a1为1
在实践中 尽量保持声明顺序和初始化列表顺序保持一致
2 隐式类型转化
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
int main()
{
A a1(1);//构造
A a2 = a1;//拷贝构造
A a3 = 3;//隐式类型转化 内置类型转换成自定义类型
const A& a33 = a3;//转化是会产生临时变量 具有常性 需要用const 修饰 避免权限的扩大
const A& a4 = 3;//这里能够通过是 通过构造函数构造了3的临时变量 然后a4是给3的临时变量取别名
return 0;
}
且在创建a3中的过程中 3首先通过构造函数将自己进行转换为相关结构临时变量 之后通过拷贝构造将临时变量赋值给a3 这其中经历两个过程 但是编译器会进行优化为构造 也就是 构造加拷贝构造会直接优化为构造
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class stack
{
public:
void push(const A & aa)
{
}
};
int main()
{
stack st;
A a1(2);
st.push(a1);
A a2(4);
st.push(a2);
//
st.push(2);
st.push(4);
return 0;
}
应用类型转化可以更快更方便的完成操作
3 explicit关键字
3.1 单参构造函数,没有使用explicit修饰,具有类型转换作用
explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
3.2多参数构造函数
在正常情况下aa2是无法构造的 这种形式不允许。
但如果存在单构造函数是可以运行的 原因就是aa2去调用了单构造函数。且会将aa2中的成员a赋值为3。
解决方式是可以用花括号写。 这里也是构造产生临时变量 拷贝构造给 aa2
这里是通过构造生成 临时变量 之后在用aa3来引用临时变量来取别名
class A
{
public:
//A(int a)//单参构造函数
// :_a(a)
//
//{}
A(int a,int a1,int a2)//多参构造函数
:_a(a)
,_a2(a1)
,_a3(a2)
{}
private:
int _a;
int _a2;
int _a3;
};
class stack
{
public:
void push(const A & aa)
{
}
};
int main()
{
stack st;
A aa1 = { 1,2,3 };
st.push(aa1);
st.push({ 1,2,3 });//这里没有先调用push 而是先调用构造函数去创建临时变量 之后在调用push函数
return 0;
}
所以多参数构造函数也支持隐式类型转化 通过花括号实现 用explicit修饰构造函数,将会禁止构造函数的隐式转换。
这里缺省值不仅可以给整数 还可以直接malloc空间 以及给自定义类型缺省值 这样声明即使是自动生成的默认初始化列表也可以进行初始化 在调试时 会从声明中一个个过 但实际上还是从初始胡列表中一个个过 这是因为从声明上经过更好展示调试过程
4. static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的 成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
正常的成员函数是可以调用静态成员的 因为静态成员声明和定义分离 即使 声明在私有区也可以正常使用 如果只有声明没有定义是会报错的
在静态成员公有的情况下 ,可以通过上图中进行访问静态成员
应用 可以用于统计先情况有多少个对象存活
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { }
static int GetACount() { return _scount; }
private:
static int _scount;
};
int A::_scount = 0;
void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
A FUNC()
{
A aa1;
return aa1;
}
int main()
{
TestA();
FUNC();
cout << A::GetACount();
return 0;
}
我们上面所讲的内容 都是建立在静态成员时公有的情况下 如果静态成员时私有的 这时候就无法访问了 这时我们提供了静态成员函数 来进行访问静态成员变量
静态成员函数没有this指针是无法访问成员变量的 只能访问静态成员变量
因为我们的的成员变量都是通过this指针去进行访问的
如何去调用静态成员函数
直接进行调用
特性
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
5. 友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多 用。 友元分为:友元函数和友元类
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声 明,声明时需要加friend关键字。
说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数 友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。 友元关系是单向的,不具有交换性。 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time
类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
这时data就可以直接访问time 但是time能否访问data就不能确定了
就好比我喜欢你 我把你当做朋友可以随时随地的来访问我 而你确不一定喜欢我 不一定把我当做你的朋友 不能认为你对我好 那么我就该理所当然的对你好
友元关系不能传递 如果B是A的友元,C是B的友元,则不能说明C时A的友元。
友元关系不能继承。
总结 友元不要多用 在一定程度上破坏了封装
内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外 部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。(两者是类似于平行的关系) 注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中 的所有成员。但是外部类不是内部类的友元。 特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系。