目录
(2)例子2:自定义类型有默认构造函数,则不用在初始化列表初始化
(1)解释构造函数的隐式类型转换 Date d2 = 2022;
(2)解释 const Date& d6=2022 必须加const才对
2.静态成员变量的OJ题:JZ64 求1+2+3+...+n
一.流插入<<与流提取>>
1.他们能自动识别内置类型的原因:函数重载
重载了各种内置类型的<<运算符重载函数,所以遇到不同内置类型时,会自动匹配对应的函数重载
cout类型是ostream(out)
cin类型是istream(in)
2.重载日期类的流插入 cout <<
Date.h:
#include <iostream>
#include <assert.h>
using std::cout;
using std::cin;
using std::endl;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void operator<<(std::ostream& out) //cout的类型是ostream
{
out << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
test.cpp:
void TestDate7()
{
Date d1;
d1.operator<<(cout);
d1 << cout; //重载写法颠倒了,虽然能正常运行,但格式很难看,正常应该是cout<<d1
//int i = 1;
//double d = 2.2;
//cout << i << endl;
//cout << d << endl;
}
int main()
{
TestDate7();
return 0;
}
(1)<<重载函数 不能写在类中,原因:
如果要把 <<函数重载 写在日期类中,日期类默认有this指针,调用void operator<<(std::ostream& out)时因为是双目运算符,第一个参数默认传给this指针,第二个参数传给形参,所以调用时只能写成d1 << cout; ,这样虽然是对的,但是为了保持可读性,正常的写法应该是 cout<<d1 ,所以必须想办法把两个参数反过来。
(2)<<重载函数 写在类外 要搭配 友元
如果把<<函数重载 写在类外 ,那他将无法访问对象的私有成员变量_year等,所以要在类中加上友元,友元:表明这个外部函数是类的朋友,可以随意访问私有成员变量。但这仍然有问题
(这样仍是存在问题的,未完善)
Date.h:
class Date
{
friend void operator<<(std::ostream& out, const Date& d1);
public:
Date(int year = 1, int month = 1, int day = 1);
private:
int _year;
int _month;
int _day;
};
void operator<<(std::ostream& out,const Date& d1)
{
out << d1._year << "-" << d1._month << "-" << d1._day << endl;
}
(3)<<重载函数 写在类外不能写在头文件中
若写在头文件中,Date.cpp和test.cpp都包含了Date.h,void operator<<(std::ostream& out,const Date& d1) 将会在两个cpp文件展开定义,编译生成两个.o文件,Date.o和test.o的符号表都有这个函数的定义,链接时会冲突,重定义。
那类是在.h文件,类中定义的函数会重定义吗?
答:不会,类中成员函数默认是内联,函数定义不放在头文件中,调用时因为是内联,有函数定义,call指令后面就会有函数的地址,所以就不用链接时去找函数地址,并且有函数定义就根本不用把函数放在符号表里;有函数声明时,call指令后面没有函数地址,只有做过处理的函数名,链接时通过函数名去符号表找函数地址,这样也是可以的。
重定义的解决方案:把<<重载函数定义写在Date.cpp中,Date.h展开就只有函数声明了,call指令后面没有函数地址,只有做过处理的函数名,链接时通过函数名去符号表找函数地址。
(仍需要完善)
Date.h:
class Date
{
friend void operator<<(std::ostream& out, const Date& d1);
public:
Date(int year = 1, int month = 1, int day = 1);
private:
int _year;
int _month;
int _day;
};
Date.cpp:
#include "Date.h"
Date::Date(int year , int month , int day )
{
_year = year;
_month = month;
_day = day;
}
void operator<<(std::ostream& out, const Date& d1)
{
out << d1._year << "-" << d1._month << "-" << d1._day << endl;
}
(4)为支持连续流插入需要设置返回值(完善版本)
完善版本:
Date.h:
class Date
{
friend std::ostream& operator<<(std::ostream& out, const Date& d1);
public:
Date(int year = 1, int month = 1, int day = 1);
private:
int _year;
int _month;
int _day;
};
Date.cpp:
#include "Date.h"
Date::Date(int year , int month , int day )
{
_year = year;
_month = month;
_day = day;
}
std::ostream& operator<<(std::ostream& out, const Date& d1)
{
out << d1._year << "-" << d1._month << "-" << d1._day << endl;
return out;
}
test.cpp:
void TestDate7()
{
Date d1,d2;
cout << d1 << d2;
operator<<(cout, d1);
}
int main()
{
TestDate7();
return 0;
}
3.流提取>> (和流插入<<搭配)
流提取>>类似流插入,只是因为要把值输入到d1中,所以d1不能带const 这一点差别
Date.h:
#include <iostream>
#include <assert.h>
using std::cout;
using std::cin;
using std::endl;
class Date
{
//流插入<<
friend std::ostream& operator<<(std::ostream& out, const Date& d1);
//流提取>>
friend std::istream& operator>>(std::istream& in, Date& d1);
public:
Date(int year = 1, int month = 1, int day = 1);
private:
int _year;
int _month;
int _day;
};
Date.cpp
#include "Date.h"
Date::Date(int year , int month , int day )
{
_year = year;
_month = month;
_day = day;
}
std::ostream& operator<<(std::ostream& out, const Date& d1)
{
out << d1._year << "-" << d1._month << "-" << d1._day << endl;
return out;
}
std::istream& operator>>(std::istream& in, Date& d1)
{
in >> d1._year >> d1._month >> d1._day;
return in;
}
test.cpp
void TestDate7()
{
Date d1,d2;
cin >> d1 >> d2;
cout << d1 << d2;
}
int main()
{
TestDate7();
return 0;
}
二.初始化列表
引入:
初始化方式:
![](https://img-blog.csdnimg.cn/0e7fd79ed9854ae99e7e4e7f55239614.png)
为什么要有初始化列表:
普通成员变量_year,_month,_day,这些可以在定义时初始化,也可以定义时不初始化,后面再赋值修改,但 引用,const,自定义类型(无默认构造函数)的成员变量必须在定义时初始化
int value = 10;
class A
{
public:
A(int x)
:_x(x)
{}
private:
int _x;
};
class Date
{
public:
//初始化列表可以认为就是对象的成员变量定义的地方
Date(int year, int n, int a)
:_n(n)
, _ref(value)
, _aa(a)
{
_year = year;
//_n = n; 这样是错的
//_ref = ref;
}
private:
// 定义时初始化
// 也可以定义时不初始化,后面再赋值修改
int _year; // 声明
// 只能在定义初始化
const int _n;
int& _ref;
A _aa;
};
int main()
{
Date d1(2022, 1, 10); // 对象定义
return 0;
}
1.定义
初始化列表:就是对象的成员变量定义的地方
2.格式
格式: 成员变量()
成员变量=0;这样直接赋值是错的,会报错
int value = 10;
class Date
{
public:
Date(int year, int n, int a)
:_n(n)
, _ref(value)
, _year(year)
, _aa(a)
{
// _year=year; //_year也可以在函数体内定义
}
private:
int _year;
const int _n;
int& _ref;
A _aa;
};
int main()
{
Date d1(2022, 1, 10); // 对象定义
return 0;
}
3.特征
原因:这三个都是必须在定义的时候初始化,而且初始化列表是变量定义的地方,所以必须在初始化列表初始化,其他的变量既可以在初始化列表初始化,也可以在函数体内初始化
那为什么这三个成员必须在定义时初始化?:
①引用成员变量你上来定义个引用,你必须得说清楚这个引用是谁的别名,如果你不在定义时初始化它,那就是没说清楚这个引用是谁的别名,比如例子1中 int& _ref 是value的别名,所以把 value 赋给 _ref ,即:在初始化列表中初始化_ref :_ref(value)
②const成员变量只有一次初始化机会,初始化后不得修改,如果你定义时没有初始化,那你后面也无法修改,那这个const成员变量就没有被初始化。
③自定义类型成员初始化时必须调用构造函数,如果你定义时没有初始化(就是没有在初始化列表初始化 -> 相当于例子1中的_aa(a)不写),正常情况走读初始化列表时会自动调用 自定义类型成员_aa 的默认构造函数去初始化他自己,_aa(a)不写=没传参,没传参也可以正常初始化,如果没有默认构造函数,就只能调用普通带参构造函数(例子1中的A(int x) 就是普通带参构造函数),你_aa(a)都没写,参数没传,根本调用不动普通带参构造函数。即:调用普通带参构造函数需要显示的写参数,这个问题详情请见 C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解_beyond.myself的博客-CSDN博客 大标题一 —>小标题2. —>(7)
(1)例子1:都初始化
初始化d1需要把初始化列表走一遍进行初始化
int value = 10;
class A
{
public:
A(int x) //无默认构造函数,这个是普通带参构造函数
:_x(x)
{}
private:
int _x;
};
class Date
{
public:
Date(int year, int n, int a)
:_n(n)
, _ref(value)
, _year(year)
, _aa(a)
{}
private:
// 定义时初始化
// 也可以定义时不初始化,后面再赋值修改
int _year; // 声明
// 只能在定义初始化
const int _n;
int& _ref;
A _aa;
};
int main()
{
Date d1(2022, 1, 10); // 对象定义
return 0;
}
(2)例子2:自定义类型有默认构造函数,则不用在初始化列表初始化
因为走初始化列表时会自动调用_aa的默认构造函数去初始化他自己。
int value = 10;
class A
{
public:
A(int x=0) //有默认构造函数
:_x(x)
{}
private:
int _x;
};
class Date
{
public:
Date(int year, int n, int a)
:_n(n)
, _ref(value)
, _year(year)
//, _aa(a) 就不用写这个了
{}
private:
int _year;
const int _n;
int& _ref;
A _aa;
};
int main()
{
Date d1(2022, 1, 10);
return 0;
}
(3)例子3:
有一个类A,其数据成员如下: 则构造函数中,成员变量一定要通过初始化列表来初始化的是哪些?
class A {
...
private:
int a;
public:
const int b;
float* &c;
static const char* d;
static double* e;
};
a是不同数据成员,可以通过构造函数进行赋值
b,c : const常量以及引用只能通过初始化列表初始化
d,e 是静态成员,只能在类外初始化
结论:所以答案是b,c 只能通过初始化列表初始化。
(4)例子4:缺省值和初始化列表
class Stack
{
public:
Stack(int capacity = 0)
{
_a = (int*)malloc(sizeof(int) * capacity);
assert(_a);
_top = 0;
_capacity = capacity;
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
MyQueue(int size = 1)
:_size(size)
{}
/*MyQueue(const MyQueue& q)
{}*/
//拷贝构造也是构造函数,你写了拷贝构造就不会生成默认构造函数,所以MyQueue无可以使用的
//默认构造函数,就会报错
private:
Stack _st1;
Stack _st2;
size_t _size = 0; // 缺省值
};
int main()
{
MyQueue mq;
return 0;
}
初始化时会把初始化列表走一遍,挨个初始化成员变量,在初始化列表中先调用_st1和_st2的默认构造函数把他们初始化,再看初始化列表有_size(size),所以就把_size赋成1,如果初始化列表啥也没有,就把_size赋成缺省值0,
总结就是:初始化列表有值就给值 缺省值就没用了,初始化列表没值就给缺省值。如果缺省值和初始化列表都没给值,那对内置类型不做处理,_size为随机值。(缺省值还是属于在初始化列表初始化的)
(5)对特征4.的解释
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。先按类声明顺序初始化完初始化列表里的变量,再初始化函数内的成员变量
比如下面这个:因为类A有默认构造函数,则可以不用在初始化列表初始化成员变量_aa,因为走读初始化列表时会自动调用_aa的默认构造函数去初始化他自己。
分析初始化顺序:
看类中声明顺序,_year虽然排第一,但是他是在函数体内定义,所以肯定在走完初始化列表再初始化。所以初始化列表中 ①先初始化_n=1。 ②再初始化_aa,去调用他的类的默认构造函数去初始化_aa=0。 ③再初始化_ref=10。④最后结束初始化列表,走到函数体内初始化,初始化_year=2022。⑤定义了一个变量aa=10(因为实参a是10),又把_aa赋成aa,也就是_aa=10。_aa前面走的是默认构造函数,函数体内走的是赋值初始化。
int value = 10;
class A
{
public:
A(int x = 0) //有默认构造函数
:_x(x)
{}
private:
int _x;
};
class Date
{
public:
Date(int year, int n, int a)
:_n(n)
, _ref(value)
{
_year=year;
A aa(a);
_aa = aa;
}
private:
int _year;
const int _n;
A _aa;
int& _ref;
};
int main()
{
Date d1(2022, 1, 10);
return 0;
}
再看一个选择题:
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。初始化列表中的初始化顺序要看成员变量在类中声明次序,与其在初始化列表中的先后次序无关。所以在初始化列表中先初始化_a2,再初始化_a1,选D。
三.explicit关键字
1.用途:
用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
2.构造函数的隐式类型转换
class Date
{
public:
Date(int year) //构造函数
:_year(year)
{
cout << "Date(int year)" << endl;
}
/*explicit Date(int year)
:_year(year)
{
cout << "Date(int year)" << endl;
}*/
Date(const Date& d) //拷贝构造函数
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
};
int main()
{
int i = 0;
const double& d = i;
Date d1(2022); // 调用构造函数
// 隐士类型的转换
Date d2 = 2022; // 1.构造 + 拷贝构造 -> 优化 合二为一
Date d3(d1); // 2.调用拷贝构造函数
Date d4 = d1; // 3.调用拷贝构造函数
const Date& d6=2022
return 0;
}
(1)解释构造函数的隐式类型转换 Date d2 = 2022;
本质是隐式类型转换:隐式类型转换本质又是生成一个中间的临时变量,int 类型的2022构造 Date 类型的临时变量,临时变量再拷贝构造给d2。原本是先构造再拷贝构造,通过编译器优化就成了直接构造。
若给构造函数加上explicit,将会禁止构造函数的隐式转换,Date d2 = 2022; 就会报错
(2)解释 const Date& d6=2022 必须加const才对
构造函数隐式类型转换,2022先构造一个临时变量,然后让d6成为这个临时变量的引用,但是临时变量具有常性,所以必须加上const才正确。(权限缩小和放大规则:适用于引用和指针间,权限不适用于普通赋值)
(3)构造函数的隐式类型转换的语法的意义
#include<string.h>
//库中的string类
//class string
//{
//public:
// string(const char* str)
// {}
//};
void Func(const std::string& s)
{
}
int main()
{
string s2 = "hello";
string s1("hello"); //正常情况是构造一个对象,再把对象传参,但是利用构造函数隐式类型-
Func(s1); //-转换的语法可以写成下面的写法
Func("hello"); //把字符串直接传参就很灵活
return 0;
}
(4)类似隐式类型转换的另一种优化
匿名对象只存在于构造该对象的那行代码,离开构造匿名对象的那行代码后立即调用析构函数。
下面分别调用了多少次构造函数,拷贝构造函数,析构函数?
答:调用1次构造,调用3次拷贝构造函数,调用4次析构函数
class Weight
{
public:
Weight()
{
cout << "Weight()" << endl;
}
Weight(const Weight& w)
{
cout << "Weight(const Weight& w)" << endl;
}
Weight& operator=(const Weight& w)
{
cout << "Weight& operator=(const Weight& w)" << endl;
return *this;
}
~Weight()
{
cout << "~Weight()" << endl;
}
};
Weight f(Weight u)
{
Weight v(u);
Weight w = v;
return w;
}
int main()
{
//Weight x;
//f(x);
f(Weight()); // Weight(); 匿名对象,声明周期只在这一行
return 0;
}
f(Weight()); 中实参是匿名对象,本应该是先构造匿名对象,随后传值传参时把实参拷贝构造给形参u,但由于编译器优化,把先构造后立刻拷贝构造优化为直接构造,实参匿名对象和形参u合二为一,直接构造一个对象,所以相对于先构造一个对象x,再用x传参,这里是少调用了一次拷贝构造和一次析构函数。(x传参以及更多优化题目详解详情见http://t.csdn.cn/k9Psm 大标题三,小标题4(2),(3),(5))
(5)只要支持传单个参数的构造函数,都支持隐式类型转换
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1=2022;
return 0;
}
这里是全缺省,但仍支持传单个参数,则仍遵循 单参构造函数支持隐式类型转换
四.static成员
1.概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化
2.使用的例子:
//int count1 = 0;
//int count2 = 0;
class A
{
public:
A()
{
++_count1;
}
A(const A& aa)
{
++_count2;
}
private:
// 静态成员变量属于整个类,属于类的所有对象
static int _count1;
static int _count2; // 声明
int _a;
};
我
3.静态成员变量和静态成员函数的特征
4.静态成员的访问形式
(1)静态成员变量公有时:
//统计A类型对象创建了多少个?构造和拷贝构造各调用了多少次?
class A
{
public:
A()
{
++_count1;
}
A(const A& aa)
{
++_count2;
}
//private:
// 静态成员变量属于整个类,所以类的所有对象
static int _count1;
static int _count2; // 声明
int _a;
};
// 定义
int A::_count1 = 0;
int A::_count2 = 0;
A Func(A a)
{
A copy(a);
return copy;
}
int main()
{
A a1;
A a2 = Func(a1);
//静态成员变量属于整个类,所以类的所有对象
cout << a1._count1 << endl;
cout << a1._count2 << endl;
cout << a2._count1 << endl;
cout << a2._count2 << endl;
cout << A::_count1 << endl;
cout << A::_count2 << endl;
}
构造函数调用了1次(看count1);拷贝构造调用了3次(看count2);
(2)静态成员变量私有怎么访问?
为了突破类域,需要在类中设置成员函数去返回私有静态成员变量。
但成员函数调用还需要有对象才能调用,如果没对象呢?——没对象就只能用静态成员函数
静态成员函数特征:
①静态成员函数没有隐藏的this指针
②不能访问任何非静态成员(因为没有this指针)
class A
{
public:
A()
{
++_count1;
}
A(const A& aa)
{
++_count2;
}
// 成员函数也可以是静态,static成员函数没有this指针
static int GetCount1()
{
//_a = 0; //不能访问非静态成员,因为没有this指针(这个写法错误)
// A aa; //成员函数中再定义对象,用对象访问私有成员变量是可以的(这个写法正确)
// aa._a = 0;
return _count1;
}
static int GetCount2()
{
return _count2;
}
private:
// 静态成员变量属于整个类,所以类的所有对象
static int _count1;
static int _count2; // 声明
int _a;
};
// 定义
int A::_count1 = 0;
int A::_count2 = 0;
A Func(A a)
{
A copy(a);
return copy;
}
int main()
{
A a1;
A a2 = Func(a1);
cout << a1.GetCount1() << endl; 如果有对象,静态/非静态变量都能调用
cout << a2.GetCount2() << endl; 这里不会传递指针,不会传a1地址,因为没有this指针,
仅仅为了指定类域
cout << A::GetCount1() << endl;
cout << A::GetCount2() << endl;
}
五.C++11 的成员初始化新玩法。
1.C++11支持非静态成员变量在声明时进行初始化赋值
有以下三种写法:
class B {
public:
B(int b = 0)
:_b(b)
{}
int _b;
};
class A {
public:
void Print()
{
cout << a << endl;
cout << b._b << endl;
cout << p << endl;
}
private:
// 非静态成员变量,可以在成员声明时给缺省值。
int a = 10; //1.正常给缺省值的写法
B b = 20; //2.内置类型通过 隐式类型转换 给缺省值的写法
int* p = (int*)malloc(4); //3.声明给函数初始化的写法
static int n; //静态成员变量不可以给缺省值,原因在下面
};
int A::n = 10;
int main()
{
A a;
a.Print();
return 0;
}
静态成员变量不可以给缺省值原因:
比如:static int n=10; 这样写就是错的
原因:静态成员变量只有声明在类中,定义在类外,如果你给了缺省值,但是定义没写,静态成员变量也不会在初始化列表初始化;非静态成员变量定义在构造函数的初始化列表中,就算您不写构造函数,也会自动生成默认构造函数,反正 非静态成员变量 肯定会在类中定义,所以他们可以给缺省值。
2.静态成员变量的OJ题:JZ64 求1+2+3+...+n
题目链接:求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)
(1)答案:用类Sum构建一个变长数组a[n],数组中有n个对象,构造n个对象需要调用n次构造函数,再通过静态变量 _i 记录构造调用是第几次,静态变量_ret把每一次的 _i 加起来,一直加到n,就相当于了1+2+……+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 a[n];
return Sum::GetRet();
}
};
(2)利用内部类的友元关系
class Solution {
private: //把sum设为私有,只有自己可以使用,防止别人使用
class Sum //Sum是Solution的内部类,Sum是Solution的友元
{
public:
Sum()
{
_ret+=_i;
++_i;
}
};
public:
int Sum_Solution(int n) {
Sum a[n];
return _ret;
}
private: //把 _i 和 _ret 由Sum变成Solution的成员变量,这样Sum和Solution都可以访问
static int _i;
static int _ret;
};
int Solution::_i=1;
int Solution::_ret=0;