引用
引用其实就是变量的别名,二者共用一个存储空间
1 | int b=1; |
引用一旦被声明就不可以改变其所代表的参数,任何对引用变量的操作实际上都是对原变量的操作。
这里我们声明了 y
是 x
的引用,之后我们希望 y
变成 z
的引用,但是结果却是我们只是改变了 x
的值,二者地址仍然是一样的。
1 | int x=10; |
于是我们更加深入地思考引用的本质,其本质就是一个常指针。
注意这里 const
的位置,不变的是 p
而不是 *p
。
1 | int & b = a ;// int * const b = & a; |
由于引用本质上是对地址的作用,因此如果我们希望直接引用一个常数是做不到的。
1 | int & a =100;//错误的。因为常数100没有其地址 |
引用和指针有相似之处也有不同点。
1 | //引用声明时必须赋值,指针不必 |
最后是引用的一些用法
作为参数
1
2
3
4
5
6void swap (int & a,int & b){
int c=a;
a=b;
b=a;
return;
}作为函数返回值
此时返回值不能是局部变量,否则函数结束后会释放该地址的内容!
1
2
3
4int & fun(){
static int c=10;
return c;
}
Class
数据分配
非静态成员变量,在每一个对象实例中被分配单独空间。
静态成员变量,会被分配空间,但是不在对象实例所占用的空间内而是处于堆区,所有对象实例共用
成员函数,仅是声明,不在对象实例的空间内。
因此
sizeof()
函数作用于某个对象实例时的返回值是该类所有非静态成员变量内存之和。
构造函数快速初始化
1 | class C{ |
拷贝构造函数
使用情景
- 根据已经建立的实例来初始化另一个实例。
- 函数传参
- 函数返回局部对象
深拷贝与浅拷贝
浅拷贝即直接将原来参数的地址传给新参数。
深拷贝是指重新开辟空间,之后在空间中写入参数值。
一般情况下两种拷贝没必要区分,但是在需要反复开辟释放空间的地方要格外小心。
1 | class C{ |
注意事项
拷贝构造函数的应当使用引用
1 | C (const C& C1){ |
这和拷贝构造函数的使用情景相关,假使我们选用的参数是 const C C1
,则根据拷贝构造函数的使用规则,传入参数是需要调用该函数的,那么就会发生无穷递归最终程序崩溃。而通过引用相当于直接传入地址,就可以避免这一情况。至于const
则是为了防止不必要的失误导致传入的实例被更改。
静态成员变量
以static
作为关键字的成员变量,其特点是类内声明,类外初始化
1 | class C{ |
以static
为关键字的成员函数,其特点是只能调用对象的静态成员变量。这是因为静态成员函数的初始化是在编译阶段进行的,此时计算机只给静态变量分配了空间而对于一般变量,程序还不知道其地址。
对于public
的静态成员变量,可以在类外内通过C::sa
来访问,而private
类型的静态成员变量则不可在类外访问。
this指针
this
指针是每一个对象实例被初始化时自带的一个指针,不需要自己定义。其使用场景如下
形参和成员变量同名
this->a=a
函数返回值是当前对象
return *this
空指针访问成员函数
如果该成员函数没有用到this
指针,则访问没问题,否则失败。
判断一个成员函数中有没有用到this
指针最直接的方法就是观察函数中是否出现成员变量。
综上,如果我们用一个对象指针变量访问类函数时,需要保证该指针不是空指针,或者更规范地,在函数中加入
1 | if (p==NULL) return ; |
const修饰的成员函数
常函数是指在函数后面添加const
关键字的成员函数
1 | void fun() const { |
1 | class C{ |
可以看出,常函数是不能改变成员变量的,但是如果成员变量前面加了mutable
关键字,就可以在这里改变。
常函数通常存在于常对象。常对象只能调用常函数。
1 | class C{ |
友元
通过添加友元的方式,可以直接访问对象的私有成员。(在类A
中声明B
是友元,相当于B
能访问A
的私有成员,反之不能)
友元的使用情形大致分为三种:
全局函数作为友元
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class C{
private:
int a;
int b;
public:
C():a(1),b(2){}
friend void Print(C & c);
};
void Print(C & c){
cout<<c.a<<" "<<c.b<<endl;
}
int main(){
C c;
Print(c);
return 0;
}类作友元
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class C{
friend class B;
private:
int a;
int b;
public:
C():a(1),b(2){}
};
class B{
public:
void print(C& c){
cout<<c.a<<" "<<c.b<<endl;
}
};
int main(){
C c;
B b;
b.print(c);
return 0;
}成员函数作友元
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class C;
class B{
public:
void print1(C& c);
void print2(C& c);
};
class C{
friend void B::print1(C& c);
private:
int a;
int b;
public:
C():a(1),b(2){}
};
void B::print1(C& c){
cout<<c.a<<" "<<c.b<<endl;
}
void B::print2(C& c){
cout<<c.a<<" "<<c.b<<endl;
}
int main(){
C c;
B b;
b.print1(c);
b.print2(c);
return 0;
}这里只有
B::print1()
可以正常输出,b::print2()
因为没有声明友元而报错。这种对于类的某些成员函数的友元声明在之后重载
cout
函数中还会用到。
运算符重载
自增(减)
cout
(cin
)()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using namespace std;
class B{
friend ostream& operator << (ostream& cout,B b);
private:
int a;
int b;
public:
B():a(2),b(3){
}
B& operator ++ ();//前置
B operator ++ (int);//后置
void operator () ();//函数调用符
};
B& B::operator ++(){
cout<<"++b : ";
this->a++;
this->b++;
return *this;
}
B B::operator ++(int){
cout<<"b++ : ";
B temp=*this;
this->a++;
this->b++;
return temp;
}
void B::operator()(){
cout<<"load operator () : "<<this->a<<" "<<this->b<<endl;
}
ostream& operator <<(ostream& cout,B b){
cout<<b.a<<" "<<b.b<<endl;
}
int main(){
B b;
b();
cout<<b;
cout<<b++;
cout<<++b;
return 0;
}
注意事项
- 前置
++
由于是先加后用,因此返回的是当前对象自增后的引用。 - 后置
++
是先用后加,因此函数返回值应该是自增前自身的一个拷贝,该拷贝在函数结束时释放,故不能返回其引用。 cout<<b++
在使用时要注意自己重载时的数据类型,比如重载时参数为B& b
,则不能输出,因为b++
的返回值不是引用。- 重载类
B
的operator << ()
是重载左移运算符,想要用cout
输出对象,要重载的应该是ostream
的左移运算符。 ()
重载在之后伪函数还会用到。
继承
继承的特点
- 父类的私有成员子类一律不能访问。不同的继承形态只决定父类的公用成员和保护成员在子类中的属性。
- 构造析构函数的调用顺序为:父类构造–>子类构造–>父类析构–>子类析构
- 访问父类的同名函数或成员变量需要加作用域,包括参数不同的重载函数。
- 直接访问同名成员时,返回子类成员。
- 多重继承时,子类只能访问到其直接父类,父类的父类是不能访问的。
继承的成员变量
1 | class A{ |
以上述代码为例,子类D
的空间大致可分为B
、C
、D
三块
成员变量 | 来源 | 访问方式 |
---|---|---|
B::a | 继承自B(进一步继承自A) | d.B::a |
B::b | 继承自B | d.B::b |
B::c | 继承自B(进一步继承自A) | d.B::c |
B::d | 继承自B | d.B::d |
C::a | 继承自C(进一步继承自A) | d.C::a |
C::b | 继承自C (进一步继承自A) | d.C::b |
C::c | 继承自C | d.C::c |
C::d | 继承自C | d.C::d |
D::d | 自身 | d.d |
此外还有D::B::b
和D::C::c
两个变量,但是无法访问。
虚继承
虚继承是为了减少多重继承中二义性的出现而产生的。
如上图,在菱形继承下,最终子类D
有着11
个成员变量,但是成员变量的表示符号只用了a
、b
、c
、d
四种,这就产生了二义性。
如果我们采用虚继承,即在B
、C
类的定义中加上virtual
关键字
1 | class A{ |
有了虚继承以后,二义性就消失了。
再来看看最终子类中保存的数据
1 | a=1 //该数据来自A |
即同名成员变量以最新修改的值为准。
最后讨论一些虚继承的规则(仍然以A
、B
、C
、D
的菱形继承为例)
A
中定义了变量a
,而B
、C
均没有该变量,则D
直接访问a时以A
中为准,无二义性。A
中定义了变量a
,而B
(或C
)存在同名成员变量,则D中直接访问a
以B
(或C
)为准,无二义性。A
中定义了变量a
,B
、C
都包含同名成员变量,D
中直接访问a
会产生二义性。
可以看出,一旦出现了多重继承,会大大降低程序的可读性,因此非必要不要出现多继承
虚继承内存分配
还是上面的菱形继承,我们输出一下内存
1 | int main(){ |
我们通过调试可以查看各个类的内存结构。
A
包含 a、b、c三个int
变量
B
包含A
的a、b、c以及自身的b、d,另外还有一个__vptr
变量。
C
和B
类似。
D
则分别从B
、C
中继承了这个__vptr
。
其他都好理解,关键是这个__vptr
(也有的编译器叫他它__vbptr
)。
在虚继承中,内存区域被划分为不变区和共享区。共享区也就是所谓虚基类所在的内存地址。共享区会随着子类数据的更新而更新,那么显然,子类需要一个指针来从当前地址找到虚基类的地址然后更新数据,这就是__vbptr
的作用。
将__vbptr
理解成编译器在编译阶段额外添加如虚基类的子类中的一个成员变量就可以了。
虚函数
虚函数有点像虚继承,只不过它是“虚继承”了父类的某些函数,这些函数以virtual
作为关键字。
1 | class A{ |
我们通过对A
、B
占用内存(此处用32
位,类的定义如上)的测试来引出接下来的内容
1 | int main(){ |
正常来说,A
有一个int
型变量,其余都是函数,应该占用4
字节;B
在A
的基础上又加了一个int
变量,应该是8
字节。但是测试结果正好是二者分别加4
。这里留一个悬念。
__vfptr
指针
之后再测试如下代码
1 | int main(){ |
这里产生了两个问题
- 为什么
virtual
关键字修饰前后,同一个指针p
访问到的函数不是同一个类? - 子类的析构函数?
我们引入刚才多余的四字节变量——__vfptr
,这个指针的数据类型是**void
,而它所指向的*void
数组就是解决上述问题的关键——虚函数表。
虚函数表里存放了类中所有virtual
修饰的虚函数,当程序需要执行的函数是虚函数时,__vfptr
指针会根据程序指令从虚函数表中找到对应的虚函数然后执行。
__vfptr
相当于是编译系统额外加入的一个成员变量,它所对应的虚函数表也是根据所在类生成的。
下面我们回答第一个问题。
p
是一个类A
的指针。行fun1()
时,系统根据p
的数据类型执行,因此类A的指针自然执行类A
的fun1()
。
但是当执行虚函数fun()
时,程序先找出p
所指地址中的__vfptr
变量,然后由它去查虚函数表。由于p
地址存放的是类B
,因此__vfptr
也属于类B
,所查的虚函数表自然也是类B
的表,所以程序最终执行类B
的fun()
。
第二个问题也容易理解。delete p
时,系统是不知道这个指针里面装了什么,它只知道这是一个类A
的指针,所有只执行类A
的析构函数,这就产生了隐患。
根据前面提到的,也很容易想到对应方法。只要我们把~A()
也写成一个虚函数,程序在删除p
时遇到了virtual ~A()
,就又去找__vfptr
了。找到以后自然是执行~B()
。
上述情况的症结在于,指针的数据类型和指针所指地址的实际类型不一致,一般成员函数的调用是根据指针本身的类型决定的,而虚函数则是依赖于地址内数据的类型。
于是我们想到另外一种错误。如果我们在类B
中添加函数virtual fun2()
,再给一个B* p
赋值一个类A
的实例a
然后调用fun2()
。显而易见,编译器找不到对应函数。(事实上,有些编译器不能接受将父类对象实例赋值给子类对象指针)
虚函数内存分配
不同编译器对虚函数的处理不同可能导致内存分配有所不同,下面都以 TDM-GCC 4.9.2 32bit Debug 为例
- 单继承中,最终子类只会有一个虚函数表,占用内存应该加上
sizeof(**void)
- 普通多继承中,子类会从每一个父类中继承一个
__vfptr
变量。 - 虚继承的虚函数比较复杂。它既有
__vfptr
又有__vbptr
,不过一些编译器会将二者合成为一个新指针__vptr
,所以这类编译器在内存分配上和上一种情况类似。
纯虚函数
纯虚函数是特殊的虚函数,它在父类中没有定义函数体,只是声明。
纯虚函数的基本语法为
1 | virtual 返回类型 函数名称()=0; |
纯虚函数在被重写前不能调用,因为它只是函数声明。
子类可以重新父类的纯虚函数,如果不重写,则该函数在子类中仍然是纯虚函数。
包含纯虚函数的类称为抽象类,抽象类不能实例化。
可以看出,纯虚函数有类似于模板的功能。
最后是一些关于虚函数的使用习惯
- 基类的析构函数最好是虚析构。
- 不能将静态成员函数定义为虚函数。
- 在类外定义虚函数的函数体时不需要加
virtual
关键字。 - 构造函数和友元函数不能是虚函数。
文件读写
添加头文件<iostream>
ios
1 | ios::in //读文件 |
写文件
文本文件
f<<data;
写入一个
const char*
变量f.put();
写入一个字符
1 | fstream f; |
二进制文件
f.write((char*)& data , sizeof(data) );
写入指定长度数据
f.put(bytes);
写入一个字节
1 | f.open("b.txt",ios::out|ios::binary); |
读文件
文本文件
f>>buf;
读取到空格、制表、换行停止或
sizeof(buf)
大小的数据getline(f,s)
读取一行存入
string
变量f.get()
或``f.get(c)`读取一个字符存入
char
变量f.getline(buf,size)
读取
size
大小或一行数据到buf
数组
1 | f.open("t.txt",ios::in); |
二进制文件
f.read((char*)& data ,sizeof(data));
1 | fstream f; |
文件检验函数
f.bad()
读写错误f.fail()
读写及格式错误f.eof()
结束标志f.good()
以上三个的集合f.is_open()
文件打开判定
位置指针
位置指针主要用于二进制文件读写,因为二进制文件内部每个字符大小一样,而文本文件有些特殊字符占位不同。
文件指针主要有两套
seekp()/tellp()
这两个是对写入指针的操作,前者是改变当前指针位置,后者是获取。
seekg()/tellg()
这两个是对读取指针的操作,同样前者是修改,后者是获取。
函数参数主要是一个整型和一个ios
。
1 | f.seekg(n,ios::beg); //从开头向后移动n字节 |
模板
模板声明格式
1 | template <class T> |
其中class
可以用typename
替换。
注意事项
1 | template <class T> |
- 在调用模板函数时,最好用
<>
指定数据类型;如果没有事先声明,则编译器根据传入参数的类型自动推导。 - 对于无参模板函数,必须指定数据类型。
- 编译器自动推导时,同一个参数模板
T
的形参必须是同一类型,否则编译器无法分辨应该用哪种数据类型。 - 指定数据类型后,编译器在执行函数时会隐含强制数据类型转换。
- 能直接匹配普通函数的情况下会先调用普通函数。
- 不能直接匹配普通函数,但是匹配模板函数时,才调用模板函数。
- 两种函数都不能匹配,则进行强制类型转换后调用普通函数。
类模板
1 | template <class T1,class T2=int> |
类模板在初始化实例时必须指明数据类型。
类模板的成员函数可以任意定义,但是实例化后一些函数可能会因为语法问题不能调用。
类模板可以有默认数据类型。(模板函数在一些版本中也可以,但为了兼容性,不推荐这么干)
在类外定义类模板的成员函数时,也需要用到模板
1
2
3
4
5template <class T1,class T2,...>;
ReturnType ClassName<T1,T2...>::FunName(T1 a1,T2,a2,...){
...;
...;
}
类模板也可以作为函数参数传入,此时可以有三种写法
ReturnType FunName(ClassName<Type1,Type2> & p)
```C++
template<class T1,class T2>;
ReturnType FunName(ClassName<T1,T2> & p)1
2
3
4
3. ```C++
template<class T>;
ReturnType FunName(T & p)
类模板作为父类继承时,有以下规则
子类是具体类时,必须指定父类的类型。
1
2
3
4
5
6
7
8
9
10template <class T>;
class F(){
public:
T a;
}
class S: public F<int>{
public:
int b;
}子类也是模板类,则可以继续用模板表示父类。
1
2
3
4
5
6
7
8
9
10
11template <class T>;
class F(){
public:
T a;
}
template <class T1,class T2>
class S: public F<T2>{
public:
T1 b;
}