C++初阶—类与对象(下篇)
第一章:再谈构造函数
1.1 构造函数体赋值
常量成员(const) 和 引用成员(reference) 必须在构造函数执行之前进行初始化。因为它们无法在构造函数体内重新赋值,所以必须通过初始化列表来初始化。
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
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 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 A {
public:
A(int a)
:_a(a) {
}
private:
int _a;
};
class B {
public:
B(int a, int ref)
:_aobj(a)
, _ref(ref)
, _n(10) {
}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
示例1
typedef int DataType;
class Stack {
public:
//能用初始化列表就用,但有些时候要混用
Stack(size_t capacity)
:_array((DataType*)malloc(sizeof(DataType) * capacity))
, _capacity(capacity)
, _size(0)
{
cout << "Stack()" << endl;
if (NULL == _array) {
perror("malloc申请空间失败!!!");
return;
}
//初始化数组只能在函数体内
memset(_array, 0, sizeof(DataType) * _capacity);
}
// s1(s)
Stack(const Stack& s) {
cout << "Stack(Stack& s)" << endl;
// 深拷贝
_array = (DataType*)malloc(sizeof(DataType) * s._capacity);
if (NULL == _array) {
perror("malloc申请空间失败!!!");
return;
}
memcpy(_array, s._array, sizeof(DataType) * s._size);
_size = s._size;
_capacity = s._capacity;
}
Stack& operator=(const Stack& st) {
if (this != &st) {
// ...
}
return *this;
}
void Push(DataType data) {
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack() {
cout << "~Stack()" << endl;
free(_array);
_array = nullptr;
_size = _capacity = 0;
}
private:
// 内置类型
DataType* _array;
int _capacity;
int _size;
};
//假设栈没有默认构造函数
//这里没有显式定义 MyQueue 的构造函数,编译器会自动生成一个默认构造函数。
//这个自动生成的默认构造函数会尝试使用 Stack 类的默认构造函数来初始化 MyQueue 的 Stack 成员 _st1 和 _st2。
//而 Stack 类没有无参数的默认构造函数(即 Stack()),则自动生成的 MyQueue 默认构造函数将无法初始化 Stack 成员。
class MyQueue {
Stack _st1;
Stack _st2;
};
//解决办法:为 MyQueue 类显式定义一个构造函数,并在构造函数初始化列表中初始化 Stack 成员。
class MyQueue {
public:
MyQueue(int capacity = 10)
:_st1(capacity)
,_st2(capacity) {
}
private:
Stack _st1;
Stack _st2;
};
示例2
class A {
public:
A(int a)
:_a(a) {
}
private:
int _a;
};
class Date {
public:
//初始化列表是每个成员定义的地方
//无论是否写,每个成员都要走初始化列表
Date(int year, int month, int day, int& i)
: _year(year), _month(month), _x(1), _refi(i), _a(1) {
//这里如果不初始化_a,并且A这个类自定义了默认构造函数,一样会初始化_a
//在构造函数的初始化列表阶段,如果没有添加内置类型到初始化列表中(且没有缺省值),会用随机值初始化(取决于编译器)
//对自定义类型,会调用它的默认构造函数
//如果既有默认构造,也有显示的初始化列表,使用初始化列表
//初始化顺序是按成员变量声明顺序来
//函数体内这里是赋值
_day = day;//初始化列表可以和以前的方式混用
//_x = 1;//不可以修改,不可以再次赋值或修改。只有一次赋值机会,就是定义时
//所以const成员变量只能在初始化列表中
//_refi = i;//这里是赋值,但没有初始化
}
void func() {
++_refi;
++_refi;
}
private:
int _year;//这里是每个成员的声明
int _month = 1;
int _day = 1;//这里只是给缺省值,并不是初始化。这里的缺省值是给初始化列表
//_day没有在初始化列表显示给值,所以用这里的缺省值1,
//但_month在初始化列表显示给值,所以不会用这列的缺省值1
//如果初始化列表没有显示给值,就用这个缺省值。
//如果显示给值了,就不用这个缺省值
//为什么要初始化列表
//1.const成员变量
//const 成员变量必须在对象创建时被初始化,并且初始化后不能被改变。
//const 成员变量的值必须在对象构造时确定,这意味着它们必须通过初始化列表来进行初始化,而不能在构造函数体内进行赋值。
const int _x;//const成员必须定义时初始化
//2.引用成员变量
//引用必须在初始化时绑定到一个有效的对象。引用不能在对象创建后再被改变,因此它们必须在初始化列表中进行初始化。
//引用的初始化是不可修改的,一旦绑定了对象,它就不允许改变绑定的目标。
int& _refi;
//3.自定义类型成员(且该类没有默认构造函数时)
A _a;//如果自定义类型没有默认构造函数,会在初始化时调用它的自定义构造函数
//·编译器会自动调用 A 类的构造函数来初始化 A 类的对象,只要 A 类提供了合适的构造函数(无论是默认的还是自定义的)。
//·如果 A 类只有带参数的构造函数而没有默认构造函数,Date类的构造函数必须显式地初始化 A 类的对象,否则编译器将报错。
//·如果自定义类型成员(即类的成员)没有默认构造函数,编译器将不能使用默认构造函数来初始化它。
//这要求在初始化列表中显式地提供所需的参数,以调用该类型的构造函数进行初始化。
//如果在构造函数体内尝试初始化这样的成员,将无法完成初始化,因为成员变量在构造函数体执行之前必须已被初始化。
};
int main() {
MyQueue mq;
int n = 0;
Date d1(2024, 9, 23, n);
d1.func();
cout << n << endl;
}
下面代码的结果是
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();
}
答案:D
A.输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
1.3 explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
示例1
class Date {
public:
// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
explicit Date(int year)
:_year(year) {
}
/*
// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具
有类型转换作用
// explicit修饰构造函数,禁止类型转换
explicit Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
*/
Date& operator=(const Date& d) {
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2022);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2023;
// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转换的作用
return 0;
}
示例2
class A {
public:
// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
A(int i)
:_a(i) {
cout << "A(int i)" << endl;
}
explicit修饰构造函数,禁止类型转换
//explicit A(int i)
// :_a(i) {
// cout << "A(int i)" << endl;
//}
A(const A& aa)
:_a(aa._a) {
cout << "A(const A& aa)" << endl;
}
~A() {
cout << "~A()" << endl;
}
private:
int _a;
};
class B {
public:
B(int b1, int b2)
:_b1(b1)
, _b2(b2) {
cout << _b1 << "," << _b2 << endl;
}
private:
int _b1;
int _b2;
};
struct SeqList {
public:
void PushBack(const A& x) {
// ... 扩容
_a[_size++] = x;
}
size_t size() const {
return _size;
}
// 读
const A& operator[](size_t i) const {
assert(i < _size);
return _a[i];
}
// 读/写
A& operator[](size_t i) {
assert(i < _size);
return _a[i];
}
private:
// C++11
A* _a = (A*)malloc(sizeof(A) * 10);
size_t _size = 0;
size_t _capacity = 0;
};
int main() {
A aa1(1);
//单参数构造函数的隐式类型转换
//用2调用A构造函数生成一个临时对象,再用这个对象去拷贝构造aa2。编译器会直接优化为用2直接构造
A aa2 = 2;
//A& ref = 2;//因为支持隐式类型转换,所以会生成临时对象(具有常性)。不能引用,权限放大
const A& ref = 2;
//对比下方两种插入方式
SeqList s;
A aa3(3);
s.PushBack(aa3);
SeqList s;
s.PushBack(4);
//隐式类型转换
double d = 1.1;
int i = d;
//int& j = d;//不能引用跟类型无关,而是类型转换会生成临时变量(具有常性)。加const即可
const int& j = d;
//C++11支持多参数的转换
B bb1(1, 1);
B bb2 = { 2,2 };
const B& ref = { 3,3 };
//有名对象。特点:生命周期在当前局部域
A aa4(4);
//匿名对象。特点:生命周期只在这一行
A(5);
A aa5(5);
SeqList s;
//使用隐式类型转换传参
s.PushBack(6);
//如果隐式类型转换被禁,就可以使用匿名对象。同样一行就可以完成
s.PushBack(A(7));
//匿名对象示例(打印日期)
//非匿名对象方法
Date d1(2024, 9, 4);
cout << d1;
//匿名对象方法
cout << Date(2024, 9, 4);
return 0;
}
第二章:static成员
2.1 概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
实现一个类,计算累积创建了多少个对象,正在使用的还有多少个对象
方法一:全局变量
int n = 0;//创建的
int m = 0;//正在使用的
//全局变量可能被外面随意修改
class A {
public:
A() {
++n;
++m;
}
A(const A& t) {
++n;
++m;
}
~A() {
--m;
}
private:
};
A Func(A aa) {
return aa;
}
//这里传引用返回不正确,只是为了测试引用返回和构造及拷贝构造的关系
A& Func2(A& aa) {
return aa;
}
int main() {
A aa1;
A aa2;
cout << n << " " << m << endl;//2 2
A();
cout << n << " " << m << endl;//3 2
Func(aa1);// A Func(A aa) 传值传参拷贝构造一次,传值返回拷贝构造一次。所以能用引用尽可能用引用
cout << n << " " << m << endl;//5 2
Func2(aa1);// A& Func2(A& aa) 3 2
cout << n << " " << m << endl;//3 2
return 0;
}
方法二:用类的私有成员变量统计
class A {
public:
A() {
++n;
++m;
}
A(const A& t) {
++n;
++m;
}
~A() {
--m;
}
//想要在类外面访问类里面的私有成员需要一个公有函数
//静态成员函数的特点是没有this指针
static int GetM() { return m; }
static int GetN() { return n; }
static void Print() {
//x++;//静态成员函数不能访问非静态成员变量,因为没有this指针
cout << m << " " << n << endl;
}
private:
不能用下面方法,因为每个对象都有n和m
//int n = 0;
//int m = 0;
//静态成员变量属于所有对象,属于整个类
//不能在这里给缺省值,因为缺省值是给初始化列表用的
//但是静态成员不会走初始化列表(初始化是某个成员对象的初始化),因为它不属于某个对象而是属于所有对象
//声明
static int n;
static int m;
//sizeof(A)是1,当做空类。因为n和m两个成员不存在对象里面。他们存在静态区
//int x = 0;
};
//定义
//这里是声明和定义分离,并不是在类外访问私有成员变量
int A::n = 0;
int A::m = 0;
A Func(A aa) {
return aa;
}
int main() {
A();
//A().Print();//匿名对象不能这样访问成员函数。这么做相当于又创建了一个匿名对象
A::Print();//只有函数是静态函数是才可以这样调用。不需要传this指针,只需突破类域即可
A aa1;
Func(aa1);
aa1.Print();//1 5。正在使用1个,累计创建5个
//A aa1;
//A aa2;
假设n和m是公有的,可以用下面方式访问
//cout << A::n << " " << A::m << endl;
//cout << aa1.n << " " << aa2.m << endl;//不能以单独m这种形式访问,因为它属于A这个类域
//A* ptr = nullptr;
//cout << ptr->n << " " << ptr->m << endl;//这是一种突破类域的访问方式
n和m既不在aa1和aa2对象里面,也不ptr指向的对象里面。它在静态区
//A();
return 0;
}
2.2 特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
【问题】
1. 静态成员函数可以调用非静态成员函数吗?
不可以。静态成员函数不依赖于类的实例,因此它们不能访问非静态成员函数或成员变量,因为非静态成员函数和变量是依赖于类的实例的。
2. 非静态成员函数可以调用类的静态成员函数吗?
可以。非静态成员函数属于类的一个实例,但它们可以访问静态成员,因为静态成员函数与类的实例无关,而是属于整个类的。
练习题
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句
class Sum {
public:
Sum() {
_ret += _i;// 每次构造时将 _i 加到 _ret 上
++_i; // 每次构造时递增 _i
}
static int GetRet() { //提供静态后面就可以用类名直接访问
return _ret;
}
private:
static int _i; //n次构造函数要访问同一个变量,所以用static
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
//创建Sum这个类的对象的数组,每当一个 Sum 对象被创建时,构造函数都会被调用,
//这样就会累加 Sum 类中的 _ret 静态变量。
Sum a[n];
return Sum::GetRet();
}
};
第三章:友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
3.1 友元函数
之前尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
class Date {
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day) {
}
d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
//ostream& operator<<(ostream& _cout) {
// _cout << _year << "-" << _month << "-" << _day << endl;
// return _cout;
//}
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;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main() {
Date d;
cin >> d;
cout << d << endl;
return 0;
}
说明:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
3.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。
比如下面Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
友元关系不能继承。
class Time {
// 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
friend class Date;
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(外部类)=外部类,和内部类没有任何关系。
B类受A类域和访问限定符的限制,其实他们是两个独立的类
class A {
public:
class B {
private:
int _b;
};
private:
int _a;
};//sizeof(A)等于4
假设B类不在public中,外部无法访问B类,只有A类里面可以访问B类
class A {
class B { //在C++中,如果一个类的成员既不在 public 中,也不在 private 中,那么该成员默认属于 private。
private:
int _b;
};
void func() {
B bb;
}
private:
int _a;
};
内部类是外部类的友元类
class A {
class B {
public:
void FuncB() {
A aa;
aa._a = 1;//因为B类是A类的内部类(即友元类),所以可以访问A类的私有成员
}
private:
int _b;
};
void func() {
B bb;
}
private:
int _a;
};
int main() {
cout << sizeof(A) << endl;
A aa;//可以定义
//B bb;//报错,未定义标识符B
//A::B bb;//B类在A类public中的正确定义方式
}
使用内部类方法改造上方求1+2+3+...+n题目
class Solution {
class Sum {
public:
Sum() {
_ret += _i;
_i++;
}
//该方法不在需要static int GetRet()静态函数获取_ret,因为该成员现在是Solution类
};
public:
int Sum_Solution(int n) {
Sum a[n];
return _ret;
}
private:
static int _i;
static int _ret;
};
int Solution::_i = 1;
int Solution::_ret = 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) {
//...
return n;
}
};
int main() {
A aa1;
// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
//A aa1();
// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
A();
A aa2(2);
// 匿名对象在这样场景下就很好用
Solution().Sum_Solution(10);
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;
}
void Print() const {
cout << "Print->" << _a << endl;
}
private:
int _a;
};
//示例1
void f1(A aa) {}
//示例2
void f1(const A& aa) {
//调用打印函数不需要传值传参,因为传值传参会调用拷贝构造,造成额外开销
//所以改成传引用传参,并且打印不能改变调用打印函数的对象,所以加const
//但const对象不能调用非const成员函数,所以打印函数也需要加const
aa.Print();
}
//示例3
void f1(A& aa) {
//这里的参数不加const也可以,非const对象可以调用const函数
//但是这种写法无法传匿名对象,因为匿名对象具有常性
aa.Print();
}
//示例4
void f1(const A& aa = A()) {
aa.Print();
}
//示例5
void f1(A aa) {
aa.Print();
}
int main() {
//示例1
A aa1;
//下方传值传参不是值拷贝,而是用一个已经存在的对象去初始化另一个要创建的对象(即调用拷贝构造)
f1(aa1);
//示例2
A aa1;
f1(aa1);
//示例3
A aa1;
f1(A());//A()是匿名对象,具有常性。不能调用
f1(1);//这种形式等价于上方,属于隐式类型转换
//示例4
f1(A(1));
f1(2);
f1();
//const引用会延长匿名对象生命周期,配合这行代码(A aa2;)可以看到先调用了2次构造函数,
//一次是A() 另一次是A aa2 ,之后才会调用2次析构函数
//ref出了作用域,匿名对象就销毁了
const A& ref = A();
A aa2;
//示例5
A aa1;
f1(aa1);//传值传参方式1
//A(int a)
//A(const A& aa)
//Print->0
//~A()
cout << "-------------------" << endl;
f1(A(1));//传值传参方式2
//A(int a)
//Print->1
//~A()
cout << "-------------------" << endl;
f1(1);//传值传参方式3
//A(int a)
//Print->1
//~A()
//~A()
//方式3存在隐式类型转换,在同一个表达式(即同一行,或同一调用,或连续步骤)中连续的构造和拷贝构造会被编译器优化
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;
}
void Print() const {
cout << "Print->" << _a << endl;
}
private:
int _a;
};
A f2() {
A aa;
return aa;
}
int main() {
f2();
//A(int a)
//A(const A& aa)
//~A()
//~A()
A ret1 = f2();//本来返回结束前aa拷贝构造临时对象,现在直接拷贝构造ret1
//A(int a)
//A(const A& aa)
//~A()
//~A()
cout << "---------------------" << endl;
A ret2;
ret2 = f2();
//上方不能优化,
//原因一:因为同类型(都是构造或拷贝构造)才能优化,这里涉及赋值
//原因二:不是连续步骤,这里是2个步骤
//A(int a) ret2的构造
//A(int a) aa的构造
//A(const A& aa) aa拷贝构造临时对象
//~A() 析构aa
//A& operator=(const A& aa)
//~A() 析构临时对象
//~A()
}
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;
}
void Print() const {
cout << "Print->" << _a << endl;
}
private:
int _a;
};
A f2() {
//示例1
//return A(1);
//示例2
return 2;
}
int main() {
//示例1
A ret1 = f2();
//A(int a)
//~A()
//这里与A ret1 = A(1)等价
//构造和拷贝构造是连续的
//示例2
A ret1 = f2();
//这里与A ret1 = 2 等价
return 0;
}
第七章:再次理解类和对象
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
- 用户先要对现实中洗衣机实体进行抽象---即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
- 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
- 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。
- 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
作业
1. 下面程序的运行结果是( )?
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();
return 0;
}
A.输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
答案:D
A.初始化顺序由定义类时的声明顺序决定,所以先初始化_a2,由于初始化_a2时_a1还未初始化,所以为随机值,故错误
B.程序正常运行
C.能编译通过
2. 有一个类A,其数据成员如下: 则构造函数中,成员变量一定要通过初始化列表来初始化的是:( )
class A {
//...
private:
int a;
public:
const int b;
float*& c;
static const char* d;
static double* e;
};
A.a b c
B.b c
C.b c d e
D.b c d
E.b
F.c
答案:B
A.a是不同数据成员,可以通过构造函数进行赋值
B.正确,常量以及引用只能通过初始化列表初始化
C.d,e是静态成员,只能在类外初始化
D.d是静态成员,只能在类外初始化
E.b常量只能通过初始化列表初始化,但不是最佳答案
F.c引用只能通过初始化列表初始化,但不是最佳答案
3. 关于C++类中static 成员和对象成员的说法正确的是( )
A.static 成员变量在对象构造时生成
B.static 成员函数在对象成员函数中无法调用
C.static 成员函数没有this指针
D.static 成员函数不能访问static 成员变量
答案:C
A.static成员变量在对象生成之前生成
B.普通成员函数是可以调用static函数的
C.static函数属于所有对象共享,不具备this指针
D.static函数唯一能够访问的就是static变量或者其他static函数
4. 在一个cpp文件里面,定义了一个static类型的全局变量,下面一个正确的描述是:( )
A.只能在该cpp所在的编译模块中使用该变量
B.该变量的值是不可改变的
C.该变量不能在类的成员函数中引用
D.这种变量只能是基本类型(如int,char)不能是C++类型
答案:A
A.正确,static限制了变量具有文件域
B.static变量是可以被改变的
C.可以被正常访问使用,以及通过成员来进行引用
D.静态变量也可以是自定义类型的变量
5. 一个类的友元函数能够访问类的( )
A.私有成员
B.保护成员
C.公有成员
D.所有成员
答案:D
A.可以访问,这也把一个函数声明为友元的目的
B.可以访问
C.可以访问
D.友元函数对一个类里面的所有成员,全部通吃,正确
6. 下面有关友元函数与成员函数的区别,描述错误的是?( )
A.友元函数不是类的成员函数,和普通全局函数的调用没有区别
B.友元函数和类的成员函数都可以访问类的私有成员变量或者是成员函数
C.类的成员函数是属于类的,调用的时候是通过指针this调用的
D.友元函数是有关键字friend修饰,调用的时候也是通过指针this调用的
答案:D
A.友元函数不是类的成员函数,就相当于你的朋友再亲密也不是你的家人,既然不是类成员函数,那和普通成员函数调用一样,不需要通过对象调用
B.友元的目的就是为了访问类的私有数据,成员函数可以直接访问类的私有数据
C.类的成员函数属于类,调用时其内部数据会通过this指针来调用
D.友元函数不具备this指针,更谈不上通过this调用,故错误
7. 下面程序段包含4个函数,其中具有隐含this指针的是( )
int f1();
class T {
public:
static int f2();
private:
friend int f3();
protected:
int f4();
};
A.f1
B.f2
C.f3
D.f4
答案:D
A.全局函数不具备this指针
B.static函数不具备this指针
C.友元函数不具备this指针
D.正确,普通成员方法具有隐藏的this指针
8. 累加天数
日期累加_牛客题霸_牛客网
描述
设计一个程序能计算一个日期加上若干天后是什么日期。
输入描述:
输入第一行表示样例个数m,接下来m行每行四个整数分别表示年月日和累加的天数。
输出描述:
输出m行,每行按yyyy-mm-dd的个数输出。
#include <assert.h>
#include <stdio.h>
int GetMonthDay(int year, int month) {
const static int monthArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if ((month == 2) && (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)))
return 29;
return monthArray[month];
}
int main() {
int m, year, month, day, n;
scanf("%d", &m);
while (m--) {
scanf("%d %d %d %d", &year, &month, &day, &n);
day += n;
while (day > GetMonthDay(year, month)) {
day -= GetMonthDay(year, month);
month++;
if (month == 13) {
year++;
month = 1;
}
}
printf("%04d-%02d-%02d\n", year, month, day);
}
return 0;
}
9. 打印日期
打印日期_牛客题霸_牛客网
描述
给出年分m和一年中的第n天,算出第n天是几月几号。
输入描述:
输入包括两个整数y(1<=y<=3000),n(1<=n<=366)。
输出描述:
可能有多组测试数据,对于每组数据, 按 yyyy-mm-dd的格式将输入中对应的日期打印出来。
#include <stdio.h>
int GetMonthDay(int year, int month) {
const static int monthArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if ((month == 2) && (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)))
return 29;
return monthArray[month];
}
int main() {
int year, n;
while (scanf("%d %d", &year, &n) != EOF) {
int day = 0;
int month = 1;
day += n;
while (day > GetMonthDay(year, month)) {
day -= GetMonthDay(year, month);
month++;
if (month == 13) {
year++;
month = 1;
}
}
printf("%04d-%02d-%02d", year, month, day);
}
return 0;
}
10. 日期差值
日期差值_牛客题霸_牛客网
描述
有两个日期,求两个日期之间的天数,如果两个日期是连续的我们规定他们之间的天数为两天
输入描述:
有多组数据,每组数据有两行,分别表示两个日期,形式为YYYYMMDD
输出描述:
每组数据输出一行,即日期差值
#include <bits/stdc++.h>
#include <complex>
#include <cstdio>
#include <iostream>
using namespace std;
int leap(int year) {
if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
return 1;
return 0;
}
int main() {
int day[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int year1, year2, mon1, mon2, day1, day2;
scanf("%4d %2d %2d", &year1, &mon1, &day1);
scanf("%4d %2d %2d", &year2, &mon2, &day2);
int sum1 = 0;
for (int yy = 0; yy < year1; yy++)
sum1 += leap(yy) ? 366 : 365;
day[2] = leap(year1) ? 29 : 28;
for (int mm = 1; mm < mon1; mm++)
sum1 += day[mm];
sum1 += day1;
int sum2 = 0;
for (int yy = 0; yy < year2; yy++)
sum2 += leap(yy) ? 366 : 365;
day[2] = leap(year2) ? 29 : 28;
for (int mm = 1; mm < mon2; mm++)
sum2 += day[mm];
sum2 += day2;
cout << abs(sum2 - sum1) + 1 << endl;
return 0;
}
11. 计算一年的第几天
计算日期到天数转换_牛客题霸_牛客网
描述
根据输入的日期,计算是这一年的第几天。
保证年份为4位数且日期合法。
进阶:时间复杂度:O(n) ,空间复杂度:O(1)
输入描述:
输入一行,每行空格分割,分别是年,月,日
输出描述:
输出是这一年的第几天
#include <cstdio>
#include <iostream>
using namespace std;
int leap(int year) {
if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
return 1;
return 0;
}
int main() {
int MonthArray[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int year, month, day;
scanf("%d %d %d", &year, &month, &day);
int sum = 0;
MonthArray[2] = leap(year) ? 29 : 28;
for (int mm = 1; mm < month; mm++)
sum += MonthArray[mm];
sum += day;
cout << sum << endl;
}