C++(初阶)(五)——类和对象(下)
类和对象(下)
- 类和对象(下)
- 构造函数(初始化列表)
- 类型转换
- static成员
- 求1到n的和
- 友元
- 内部类*
- 匿名对象*
- 对象拷贝时的编译器优化*
构造函数(初始化列表)
1,初始化成员变量主要使用函数体内赋值,构造函数初始化还有⼀种⽅式,就是初始化列表,初始化列
表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟⼀个放
在括号中的初始值或表达式。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
//以下就是初始化列表
//成员变量定义初始化时,初始化值可以是参数,全局变量,常量
:_year(year)
,_month(month)
,_day(day)
//注意初始化列表结尾没有;(分号)
{
//有些也可以在函数体内部初始化,可以同时使用两种方式,但是只能在初始化列表中只能出现⼀次
//_day = day;
}
private:
int _year;
int _month;
int _day;
};
2,每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是:每个成员变量定义
初始化的地方。
3,引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始
化,否则会编译报错。
int main()
{
//变量 "i" 需要初始值设定项
//C2734 “i”: 如果不是外部的,则必须初始化常量对象
//const int i;
//引用 变量 "ref" 需要初始值设定项
//“ref”: 必须初始化引用
//int& ref;
return 0;
}
class Date
{
public:
Date(int& x, int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
,const int _n
,int& _ref
//注意初始化列表结尾没有;(分号)
{
}
private:
int _year;
int _month;
int _day;
//必须在初始化列表初始化
const int _n;
int& _ref;
};
int main()
{
int x = 0;
Date d1(x);
return 0;
}
class Stack
{
public:
//自动调用栈的默认构造,即使此处全注释,编译器也会自动生成默认构造
Stack(int n = 4)
{
cout << "Stack(int n = 4)" << endl;
}
};
class MyQueue
{
public:
//但是如果Stack(int n),是带参的构造,不是默认构造,就会报错,带参数的需要显示初始化,这时候就需要初始化列表了,如下定义
//MyQueue(int n = 4)
// :_pushst(n)
// ,_popst(n)
//{
//}
private:
//自动调用栈的默认构造
Stack _pushst;
Stack _popst;
};
int main()
{
//会打印,证明了会调用栈的默认构造
//Stack(int n = 4)
//Stack(int n = 4)
MyQueue q;
return 0;
}
(小结:对于自定义类型没有默认构造,需要再初始化列表显示传参)
4,C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的
成员使⽤的。
class Date
{
public:
Date()
:_month(2)
{
cout << "Date()" << _day << endl;
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//缺省值,不是初始化,因为这里是声明,主要是给没有显示初始化的初始化列表使用,如果初始化列表没有显示初始化,就会使用默认缺省值
int _year = 1;
//比如这里_month=1缺省值不会使用
int _month = 1;
//初始化列表没有显示初始化,也没有缺省值,到底会不会初始化取决于编译器
int _day;
const int _n = 1 * 10;//可以是表达式
int * _ptr = (int*)malloc(12);
};
class Stack
{
public:
Stack(int n = 4)
{
cout << "Stack(int n = 4)" << endl;
}
private:
};
class MyQueue
{
public:
//如果此处什么也不写,也会正常初始化,其实是因为即使没有在初始化列表显示的写,也会走一遍初始化列表。
MyQueue()
{
}
private:
Stack _pushst;
Stack _popst;
int _n = 0;//缺省值,不是初始化,因为这里是声明,主要是给没有显示初始化的初始化列表使用
};
int main()
{
Date d1;
//通过this指针观察d1的_year_month_day的初始化
return 0;
}
5,尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,如果这
个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化.如果你没有给缺省值,对于没
有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有
显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构
造会编译错误。

6,初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆
关。建议声明顺序和初始化列表顺序保持⼀致。
下⾯程序的运⾏结果是什么(D)
A. 输出 1 1
B. 输出 2 2
C. 编译报错
D. 输出 1 随机值
E. 输出 1 2
F. 输出 2 1
#include<iostream>
using namespace std;
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2 = 2;
int _a1 = 2;
};
int main()
{
A aa(1);
aa.Print();
}
类型转换
1,C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
2,构造函数前⾯加explicit就不再⽀持隐式类型转换。
3,类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持
#include<iostream>
using namespace std;
class A
{
public:
// 构造函数explicit就不再⽀持隐式类型转换
// explicit A(int a1)
A(int a1)
:_a1(a1)
{
cout << "int a1" << endl;
}
//即使在这里写了拷贝构造,也不会显示,因为编译器会优化
//A(const A& a1)
//{
// cout << "const A& a1" << endl;
//}
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
//构造
A aa1(1);
//隐式类型转换
//1构造临时对象,临时对象再拷贝构造aa2,编译器优化后:直接构造
A aa2 = 1;
//可以引用A类对象
const A& aa3 = aa2;
//A& aa3 = aa2;不加const引用也可以
//也可以引用一个整型
cosnt A& aa4 = 1;
//普通引用不可以,因为要用1转换为A,需要先生成一个临时对象,而临时对象具有常性
//A& aa4 = 1;
//假设入栈一个元素,我们普通需要这样写
Stack st;
A aa10(10);
st.Push(aa10);
//但是有了类型转换,我们可以直接
st.Push(10);
return 0;
}
#include<iostream>
using namespace std;
class Stack
{
public:
void Push(A aa)
{
}
};
int main()
{
//假设入栈一个元素,我们普通需要先定义一个有名对象,再将有名对象传参,也就是这样写
Stack st;
A aa5(5);
st.Push(aa5);
//但是有了隐式类型转换,我们可以直接
st.Push(10);
return 0;
}
//_______________________________________________________________________
//但是我们在自定义类型传参时,建议使用传引用传参,所以理论上应该这样写
void Push(A& aa)
{
}
//但是在st.Push(10);调用时会出现问题,因为传参10会构造临时对象将参数唱给Push,而临时对象具有常性,所以需要加上const使之具有常性,也就是这样:void Push(const A& aa)
当然自定义类型之间的转换也和上面类似。
小结:简单来说,类型转换的意义就是简化传参的方式,不用定义有名对象
还有一点就是多参数的传参,注意从C++11才支持多参数转换
A aa6(1,1);
///{ 1, 1 }构造临时对象,临时对象再拷贝构造aa6,编译器优化后:直接构造
A aa6 = { 1, 1 };
A aa7(7, 7);
st.Push(aa7);
//但是有了类型转换,我们可以直接构造
st.Push({7, 7});
上面也提到了,如果不想支持类型转换,加上explicit就可以了。
static成员
1,⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化。
2,静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
3,⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针
class A
{
//没有this指针
static int GetACount()
{
return _scount;
}
};
4,静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。
5,⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
6,突破类域就可以访问静态成员,可以通过类名::(域操作符)静态成员或者对象.(点操作符)静态成员
来访问静态成员变量和静态成员函数。
7,静态成员也是类的成员,受public、protected、private 访问限定符的限制。
8,静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员
变量不属于某个对象,不⾛构造函数初始化列表。
局部的静态变量是在第一次运行到时才构造的,但是销毁是在main()函数结束后才销毁,局部的静态
变量销毁是在全局变量之前的
class A
{
public:
private:
// 类⾥⾯声明,生命周期是全局的
static int _scount;
};
// 类外⾯初始化
int A::_scount = 0;
int main()
{
//为公有时,访问方式
A a1;
A a2 = 2;
/*cout << a1._scount << endl;
cout << a2._scount << endl;
cout << A::_scount << endl;*/
//如果是私有,直接访问不了,可以通过公有的成员函数访问
return 0;
}
实现⼀个类,计算程序中创建出了多少个类对象。
方式一:
创建全局变量,只要调用构造或者拷贝构造,全局变量就++,实例化多少对象就++几次。
缺点是:全局变量可能会被修改,不安全。
#include<iostream>
using namespace std;
int _scount = 0;
class A
{
public: A(int i = 0)
{
++_scount;
}
A(const A& t)
{
++_scount;
}
};
int main()
{
A a1, a2;
A a3(a1);
A a4 = 2;
cout << _scount << endl;
return 0;
}
解决办法是:使用静态变量,使用方法相似。静态成员变量成为类的成员变量,生命周期是全局的,但不
属于类,属于所有对象,存储在静态区
#include<iostream>
using namespace std;
class A
{
public: A(int i = 0)
{
++_scount;
}
A(const A& t)
{
++_scount;
}
private:
//并不能在这里给值,因为这里是缺省值,要走初始化列表,但是静态成员变量初始化不走初始化列表
//static int _scount = 0;
static int _scount;
};
//类外初始化
int A::_scount = 0;
int main()
{
A a1, a2;
A a3(a1);
//此处如果不会优化,那么拷贝构造次数会+1
A a4 = 2;
//当静态成员变量是公有时的三种访问方式,但是为私有或保护时不能使用。
cout << a1._scount << endl;
cout << a4._scount << endl;
cout << A::_scount << endl;
return 0;
}
求1到n的和
求1+2+3+…+n_牛客题霸_牛客网
class Sum
{
public:
Sum()
{
_ret += _i;
_i++;
}
static int GetRet()
{
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum:: _i = 1;
int Sum:: _ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
//变长数组
Sum arr[n];
//如果不支持变长数组,可以new一个
//Sum* ptr = new Sum[n];
//delete[] ptr;
//通过静态函数访问类的私有成员变量
return Sum::GetRet();
}
};
每一份文件都会有有一个不同的静态成员变量,可以打印地址,发现地址不相同,所以在符号表中不会冲
突。
友元
为什么c++输入输出时会自动识别类型,因为运算符重载为成员函数,在输入输出时识别调用对应的成员
函数。
1,友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类声
明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。
2,外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
3,友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制。
4,⼀个函数可以是多个类的友元函数。
5,友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
6,友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。
7,友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。
8,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。
#include<iostream>
using namespace std;
// 前置声明,否则A的友元函数声明编译器不认识B
class B;
class A
{
//友元声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
//友元声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);
return 0;
}
#include<iostream>
using namespace std;
class A {
// 友元声明
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void func1(const A& aa)
{
cout << aa._a1 << endl;
cout << _b1 << endl;
}
void func2(const A& aa)
{
cout << aa._a2 << endl;
cout << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
int main()
{
A aa;
B bb;
bb.func1(aa);
bb.func1(aa);
return 0;
}
内部类*
1,如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在
全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
2,内部类默认是外部类的友元类。
3,内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考
虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其
他地⽅都⽤不了。
匿名对象*
1,⽤类型(实参) 定义出来的对象叫做匿名对象,相⽐之前我们定义的类型对象名(实参) 定义出来的
叫有名对象
2,匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
对象拷贝时的编译器优化*
1,现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返
回值的过程中可以省略的拷⻉。
2,如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编
译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译器还会进⾏
跨⾏跨表达式的合并优化。