常对象
在 C++ 中,const 也可以用来修饰对象,称为常对象。一旦将对象定义为常对象之后,就只能调用类的 const 成员(包括 const 成员变量和 const 成员函数)了。
定义常对象的语法和定义常量的语法类似:
1 | const class object(params); |
当然你也可以定义 const 指针:
1 | const class *p = new class(params); |
class
为类名,object
为对象名,params
为实参列表,p
为指针名。两种方式定义出来的对象都是常对象。
一旦将对象定义为常对象之后,不管是哪种形式,该对象就只能访问被 const 修饰的成员了(包括 const 成员变量和 const 成员函数),因为非 const 成员可能会修改对象的数据(编译器也会这样假设),C++禁止这样做。
虽然常对象中的数据成员不能被修改,但是如果想要修改可以通过修改数据成员声明为mutable。
1 |
|
也可以用于区分重载函数:
1 |
|
const成员函数的总结:
- const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以访问const对象内的所有数据成员(但不对任何数据作修改,除非是mutable的);
- 非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员;
- 作为一种良好的编程风格,在声明一个成员函数时,若该成员函数并不对数据成员进行修改操作,应尽可能将该成员函数声明为const 成员函数。
- 如果只有const成员函数,非const对象是可以调用const成员函数的。当const版本和非const版本的成员函数同时出现时,非const对象调用非const成员函数。
默认构造函数
1 | class testClass |
默认构造函数主要是用来完成如下形式的初始化的:
1 | 1 testClass classA; |
在这种情况下,如果没有提供默认构造函数,编译器会报错;
非默认构造函数在调用时接受参数,如以下形式:
1 | 1 testClass classA(12,'H'); |
- 如果没有定义任何构造函数,则编译器会自动定义默认构造函数,其形式如 testClass() {}; (比如定义了拷贝构造函数,也就不会自动生成默认构造函数)
- 定义默认构造函数有两种方式,如上述代码展示的,一是定义一个无参的构造函数,二是定义所有参数都有默认值的构造函数 ;
- 注意:一个类只能有一个默认构造函数!也就是说上述两种方式不能同时出现,一般选择 testClass(); 这种形式的默认构造函数 ;
- 只要定义了构造函数,编译器就不会再提供默认构造函数了,所以,最好再手动定义一个默认构造函数,以防出现 testClass a; 这样的错误。
拷贝构造函数
复制构造函数是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用。
复制构造函数的参数可以是 const 引用,也可以是非 const 引用。 一般使用前者,这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。一个类中写两个复制构造函数,一个的参数是 const 引用,另一个的参数是非 const 引用,也是可以的。
如果类的设计者不写复制构造函数,编译器就会自动生成复制构造函数。大多数情况下,其作用是实现从源对象到目标对象逐个字节的复制,即使得目标对象的每个成员变量都变得和源对象相等。编译器自动生成的复制构造函数称为“默认复制构造函数”。
注意,默认构造函数(即无参构造函数)不一定存在,但是复制构造函数总是会存在。
复制构造函数在以下三种情况下会被调用。
1.当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用。例如,下面的两条语句都会引发复制构造函数的调用,用以初始化 c2。
1
2Complex c2(c1);
Complex c2 = c1;注意,第二条语句是初始化语句,不是赋值语句。赋值语句的等号左边是一个早已有定义的变量,赋值语句不会引发复制构造函数的调用。例如
1
2Complex c1, c2; c1 = c2 ;
c1=c2;
2.如果函数 F 的参数是类 A 的对象,那么当 F 被调用时,类 A 的复制构造函数将被调用。换句话说,作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using namespace std;
class A{
public:
A(){};
A(A & a){
cout<<"Copy constructor called"<<endl;
}
};
void Func(A a){ }
int main(){
A a;
Func(a);
return 0;
}这样,如果形参是一个对象,那么形参的值是否等于实参,取决于该对象所属的类的复制构造函数是如何实现的。
以对象作为函数的形参,在函数被调用时,生成的形参要用复制构造函数初始化,这会带来时间上的开销。如果用对象的引用而不是对象作为形参,就没有这个问题了。但是以引用作为形参有一定的风险,因为这种情况下如果形参的值发生改变,实参的值也会跟着改变。
如果要确保实参的值不会改变,又希望避免复制构造函数带来的开销,解决办法就是将形参声明为对象的 const 引用。例如:
1
2
3
4void Function(const Complex & c)
{
...
}这种情况下,只能调用c的const成员函数和const成员。
3.如果函数的返冋值是类 A 的对象,则函数返冋时,类 A 的复制构造函数被调用。换言之,作为函数返回值的对象是用复制构造函数初始化的,而调用复制构造函数时的实参,就是 return 语句所返回的对象。例如下面的程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using namespace std;
class A {
public:
int v;
A(int n) { v = n; };
A(const A & a) {
v = a.v;
cout << "Copy constructor called" << endl;
}
};
A Func() {
A a(4);
return a;
}
int main() {
cout << Func().v << endl;
return 0;
}
构造函数的default和delete
C++11中,当类中含有不能默认初始化的成员变量时,可以禁止默认构造函数的生成,
1 | myClass()=delete;//表示删除默认构造函数 |
当类中含有不能默认拷贝成员变量时,可以禁止默认构造函数的生成,
1 | myClass(const myClass&)=delete;//表示删除默认拷贝构造函数,即不能进行默认拷贝 |
同时C++规定,一旦程序员实现了这些函数的自定义版本,则编译器不会再自动生产默认版本。注意只是不自动生成默认版本,当然还是可手动生成默认版本的。当我们自己定义了待参数的构造函数时,我们最好是声明不带参数的版本以完成无参的变量初始化,此时编译是不会再自动提供默认的无参版本了。我们可以通过使用关键字default来控制默认构造函数的生成,显式地指示编译器生成该函数的默认版本。比如:
1 | class MyClass |
有些时候我们希望限制默认函数的生成。典型的是禁止使用拷贝构造函数,以往的做法是将拷贝构造函数声明为private的,并不提供实现,这样当拷贝构造对象时编译不能通过,C++11则使用delete关键字显式指示编译器不生成函数的默认版本。比如:
1 | class MyClass |
当然,一旦函数被delete过了,那么重载该函数也是非法的,该函数我们习惯上称为删除函数。
default和delete的其他用途
上面我们已经看到在类中我们可用default和delete修饰成员函数,使之成为缺省函数或者删除函数,在类的外面,也可以在类定义之外修饰成员函数,比如:
1 | class MyClass |
而关于delete的显式删除,并非局限于成员函数,由此我们也知default是只局限作用于类的部分成员函数的。于是我们还可用delete来避免不必要的隐式数据类型转换。比如:
1 | class MyClass |
这是因为char版本的构造函数被删除后,试图从char构造MyClass对象的方式是不允许的了。但去掉这句的函数删除后,编译器会隐式的将a转换为整型使得编译通过,调用的是整型构造函数,这可能并不是你所想要的。但是如果这样:
1 | class MyClass |
将构造函数explicit后,构造函数一样的还是不能发生char的构造,因为char构造版本被删除了,但在Func的调用用,编译器会尝试将c转换为int,即Func(‘a’)会调用一次MyClass(int )构造,顺利通过编译。于是我们不提倡explicit和delete混用。
对与普通函数delete也有类型的效果。比如:
1 | void Func(int i){}; |
这里因为Func的char版本已经被删除,故Func(‘c’)会编译失败。
delete的有趣的用法还有删除operator new操作符,编码在堆上分配该类的对象如:
1 | void* operator new(std::size_t)=delete; |
另外析构函数也是可以delete的
这样做的目的是我们在指定内存位置进行内存分配时并不需要析构函数来完成对象级别的清理,这时我们可显式删除析构函数来限制自定义类型在栈上或者静态的构造。
移动构造函数
C++11中引入了移动构造函数,对象发生拷贝时不需要重新分配空间而是使用被拷贝对象的内存,从而提高代码运行效率
- C++中对象发生拷贝的场景可以分为两种,一种是被拷贝的对象还要继续使用,另一种是被拷贝的对象不再使用;第二种一般可以认为是对右值的拷贝
- &&是右值引用(即将消亡的值就是右值,函数返回的临时变量也是右值),右值可以匹配const &
- &可以绑定左值(左值是指表达式结束后依然存在的持久对象,可被赋值)
- 移动构造函数的第一个参数必须是自身类型的右值引用(不需要const,右值使用const没有意义),若存在额外的参数,任何额外的参数都必须有默认实参
- 移动构造函数构造对象时不再分配新内存,而是接管源对象的内存,移动后源对象进入可被销毁的状态,所以源对象中如果有指针数据成员,那么它们应该在移动构造函数中应该赋值为NULL
- 因为移动操作不分配内存,所以不会抛出任何异常,因此可以用noexcept指定(如果定义在类的外面,那么定义也要用noexcept指定)
当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。
如果使用左值初始化同类对象,但也想调用移动构造函数完成,有没有办法可以实现呢?
默认情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想调用移动构造函数,则必须使用右值进行初始化。C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。
1 |
|
通过执行结果我们不难得知,当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。
移动赋值函数
与移动构造函数类似,移动构造函数是拷贝函数的替代,移动赋值函数则是赋值构造函数的替代。
也是将原对象的东西赋值给新对象,然后原对象指向空
1 | void operator = (A && x){//正常情况下,返回值为A& |
初始化列表
初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。
使用初始化列表的原因:
推荐使用初始化列表,它会比在函数体内初始化派生类成员更快,这是因为在分配内存后,在函数体内又多进行了一次赋值操作。
常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化
1 | class Test1 |
注意,成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。
1
2
3
4
5
6
7
8
9
10
11
12
13class foo
{
public:
int i ;int j ;
foo(int x):i(x), j(i){}; // ok, 先初始化i,后初始化j
};
class foo
{
public:
int i ;int j ;
foo(int x):j(x), i(j){} // i值未定义
};
派生类对象赋值给基类对象
c++中经常会发生数据类型的转换,例如将 int 类型的数据赋值给 float 类型的变量时,编译器会先把 int 类型的数据转换为 float 类型再赋值;反过来,float 类型的数据在经过类型转换后也可以赋值给 int 类型的变量。
类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型(Upcasting)。相应地,将基类赋值给派生类称为向下转型(Downcasting)。
向上转型非常安全,可以由编译器自动完成;向下转型有风险,需要程序员手动干预。
1 |
|
运行结果:
1 | Class A: m_a=10 |
本例中 A 是基类, B 是派生类,a、b 分别是它们的对象,由于派生类 B 包含了从基类 A 继承来的成员,因此可以将派生类对象 b 赋值给基类对象 a。
赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。
实际上,为了执行赋值,派生类必须初始化好基类的成员变量。
派生类指针(引用)赋值给基类指针(引用)
1 |
|
运行结果:
1 | Class A: m_a=4 |
本例中定义了多个对象指针,并尝试将派生类指针赋值给基类指针。与对象变量之间的赋值不同的是,对象指针之间的赋值并没有拷贝对象的成员,也没有修改对象本身的数据,仅仅是改变了指针的指向。
将派生类指针 pd 赋值给了基类指针 pa,从运行结果可以看出,调用 display() 函数时虽然使用了派生类的成员变量,但是 display() 函数本身却是基类的。也就是说,将派生类指针赋值给基类指针时,通过基类指针只能使用派生类的成员变量,但不能使用派生类的成员函数。
概括起来说就是:编译器通过指针来访问成员变量,指针指向哪个对象就使用哪个对象的数据;编译器通过指针的类型来访问成员函数,指针属于哪个类的类型就使用哪个类的函数。(注意一个是指针,一个是指针类型)
执行pc = pd;
语句后,pc 和 pd 的值并不相等。这是因为D类先继承了B再继承了C,在内存模型中,D类实例的空间先存储了B类、再存储了C类,因此pa、pb直接指向内存空间的起始,而pc要指向C类的那一块空间, 因此在稍后一些的位置(B的结束、C的开始)。
引用在本质上是通过指针的方式实现的,基类的引用也可以指向派生类的对象,并且它的表现和指针是类似的。
1 | int main(){ |
运行结果
1 | Class A: m_a=4 |
ra、rb、rc 是基类的引用,它们都引用了派生类对象 d,并调用了 display() 函数,从运行结果可以发现,虽然使用了派生类对象的成员变量,但是却没有使用派生类的成员函数,这和指针的表现是一样的。
最后需要注意的是,向上转型后通过基类的对象、指针、引用只能访问从基类继承过去的成员(包括成员变量和成员函数),不能访问派生类新增的成员。
类、派生类的构造
类对象成员的构造:先构造成员变量中的对象,再构造自身对象(调用构造函数)
派生类构造函数:派生类可能有多个基类,也可能包括多个成员对象,在创建派生类对象时,派生类的构造函数除了要负责本类成员的初始化外,还要调用基类和成员对象的构造函数,并向它们传递参数,以完成基类子对象和成员对象的建立和初始化。
派生类只能采用构造函数初始化列表的方式向基类或成员对象的构造函数传递参数,形式如下:
派生类构造函数名(参数表):基类构造函数名(参数表),成员对象名1(参数表),…{ //…… }
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/*子类的拷贝构造函数和拷贝赋值函数*/
using namespace std;
class Person{
public:
Person(int Age,string Name):m_nAge(Age),m_strName(Name){
cout<<"Person:基类的代参构造函数"<<endl;
}
private:
int m_nAge;
string m_strName;
};
class Student:public Person{
public:
Student(int Age,string Name,int Num):Person(Age,Name),m_nNum(Num){//初始化列表传入参数,调用构造
cout<<"Student:子类的代参构造函数"<<endl;
}
Student(const Student&stu):Person(stu),m_nNum(stu.m_nNum){//初始化列表传入stu,调用拷贝构造
cout<<"Student:子类的拷贝构造函数"<<endl;
}
Student& operator= (const Student&stu){ // 子类的拷贝赋值
if(this != &stu)
{
Person::operator= (stu);//显式调用基类的operator
m_nNum = stu.m_nNum;
}
return *this;
}
private:
int m_nNum;
————————————————
原文链接:https://blog.csdn.net/whh_1218/article/details/8442734
构造函数和析构函数调用次序:
- 先构造基类
- 再构造成员
- 最后构造自身(调用构造函数)
基类构造顺序由派生层次决定:最远的基类最先构造 成员构造顺序和定义顺序符合 析构函数的析构顺序与构造相反
继承访问权限
基类中protected的成员:
- 类内部:可以访问; 类的使用者:不能访问; 类的派生类成员:可以访问(protected相比private就是能给派生类访问)
- 派生类不可访问基类的private成员,可访问基类的protected成员,可访问基类的public成员
公有(public)继承
在派生类中,基类成员在派生类的权限为:
- public -> public
- protected -> protected
- private -> 不可访问
私有(private)继承
在派生类中,基类成员在派生类的权限为:
- public -> private
- protected -> private
- private -> 不可访问
保护(protected)继承
派生方式为protected的继承称为保护继承,在这种继承方式下, 基类的public成员在派生类中会变成protected成员, 基类的protected和private成员在派生类中保持原来的访问权限。注意点:当采用保护继承的时候,由于public成员变为protected成员,因此类的使用者不可访问,而派生类可访问。
在派生类中,基类成员在派生类的权限为
- public -> protected
- protected -> protected
- private -> 不可访问
派生类对基类成员的访问形式
- 通过派生类对象直接访问基类成员
- 在派生类成员函数中直接访问基类成员
- 通过基类名字限定访问被重载的基类成员名
虚拟继承
虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继 承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如下:
1 | class A |
虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。
为什么需要虚继承?
由于C++支持多重继承,那么在这种情况下会出现重复的基类这种情况,也就是说可能出现将一个类两次作为基类的可能性。比如这里D继承B1和B2,B1继承A,B2也继承A,实际上有两条继承路径:D->B1->A,以及D->B2->A,D是一样的,但这两个A在直接继承的情况下是不一样的。当存在歧义的时候就会导致编译错误:
1 |
|
这种情况下会造成在MyClass中访问value时出现路径不明确的编译错误,要访问数据,就需要显示地加以限定。变成DerivedA::value或 者DerivedB::value,以消除歧义性。并且,通常情况下,像Base这样的公共基类不应该表示为两个分离的对象,而要解决这种问题就可以用虚 基类加以处理。如果使用虚继承,编译便正常了。
虚继承的特点是,在任何派生类中的virtual基类总用同一个(共享)对象表示。
引入虚继承和直接继承的区别
由于有了间接性和共享性两个特征,所以决定了虚继承体系下的对象在访问时必然会在时间和空间上与一般情况有较大不同:
- 时间:在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。
- 空间:由于共享所以不必要在对象内存中保存多份虚基类子对象的拷贝,这样较之多继承会节省空间。虚拟继承与普通继承不同的是,虚拟继承可以防止出现diamond继承时,一个派生类中同时出现了两个基类的子对象。也就是说,为了保证这一点,在虚拟继承情况下,基类子对象的布局是不同于普通继承的。因此,它需要多出一个指向基类子对象的指针。
内存考虑
1 | 第一种情况: 第二种情况: 第三种情况 第四种情况: |
对这四种情况分别求sizeof(a), sizeof(b):
1 | 第一种:4,12 |
每个存在虚函数的类都要有一个4字节的指针指向自己的虚函数表,所以每种情况的类a所占的字节数应该是没有什么问题的,那么类b的字节数怎么算呢?“第一种”和“第三种”情况采用的是虚继承,那么这时候就要有这样的一个指针vptr_b_a,这个指针叫虚类指针,指向虚拟基类,也是四个字节;还要包括类a的字节数,所以类b的字节数就求出来了。而“第二种”和“第四种”情况则不包括vptr_b_a这个指针。
注:关于虚函数表的内容,需要结合图片理解,推荐参考虚函数和虚函数表 - Lucky& - 博客园 (cnblogs.com)。
构造函数和析构函数的构造规则
构造函数中有默认参数的情况:既可以在类的声明中,也可以在函数定义中声明缺省参数,但不能既在类声明中又在函数定义中同时声明缺省参数。
当具有下述情况之一时,派生类可以不定义构造函数:
基类没有定义任何构造函数
基类具有缺省参数的构造函数
基类具有无参构造函数。
派生类必须定义构造函数的情况:
- 当基类或成员对象所属类只含有带参数的构造函数时,即使派生类本身没有数据成员要初始化,它也必须定义构造函数,并以构造函数初始化列表的方式向基类和成员对象的构造函数传递参数,以实现基类子对象和成员对象的初始化。
派生类的构造函数只负责直接基类的初始化
C++语言标准有一条规则:如果派生类的基类同时也是另外一个类的派生类,则每个派生类只负责它的直接基类的构造函数调用。 这条规则表明当派生类的直接基类只有带参数的构造函数,但没有默认构造函数时(包括缺省参数和无参构造函数),它必须在构造函数的初始化列表中调用其直接基类的构造函数,并向基类的构造函数传递参数,以实现派生类对象中的基类子对象的初始化。 这条规则有一个例外情况,当派生类存在虚基类时,所有虚基类都由最后的派生类负责初始化。
总结:
- 当有多个基类时,将按照它们在继承方式中的声明次序调用,与它们在构造函数初始化列表中的次序无关。当基类A本身又是另一个类B的派生类时,则先调用基类B的构造函数,再调用基类A的构造函数。
- 当有多个对象成员时,将按它们在派生类中的声明次序调用,与它们在构造函数初始化列表中的次序无关。
- 当构造函数初始化列表中的基类和对象成员的构造函数调用完成之后,才执行派生类构造函数体中的程序代码。
虚函数与抽象类
虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。
类的虚表会被这个类的所有对象所共享。类的对象可以有很多,但是他们的虚表指针都指向同一个虚表
多态性
多态就是在同一个类或继承体系结构的基类与派生类中,用同名函数来实现各种不同的功能。
- 静态绑定又称静态联编:是指在编译程序时就根据调用函数提供的信息,把它所对应的具体函数确定下来,即在编译时就把调用函数名与具体函数绑定在一起。
- 动态绑定又称动态联编:是指在编译程序时还不能确定函数调用所对应的具体函数,只有在程序运行过程中才能够确定函数调用所对应的具体函数,即在程序运行时才把调用函数名与具体函数绑定在一起。
- 编译时多态性(静态联编(连接)):系统在编译时就决定如何实现某一动作,即对某一消息如何处理。静态联编具有执行速度快的优点。在C++中的编译时多态性是通过函数重载和运算符重载实现的。
- 运行时多态性(动态联编(连接)):系统在运行时动态实现某一动作,即对某一消息在运行过程实现其如何响应。动态联编为系统提供了灵活和高度问题抽象的优点,在C++中的运行时多态性是通过继承和虚函数实现的。
虚函数
虚函数的意义:
- 首先派生类对象可以赋值给基类对象。 派生类对象的地址可以赋值给指向基类对象的指针。 派生类对象可以作为基类对象的引用。 赋值相容的问题: 不论哪种赋值方式,都只能通过基类对象(或基类对象的指针或引用)访问到派生类对象从基类中继承到的成员, 不能借此访问派生类定义的成员。
- 虚函数使得可以通过基类对象的指针或引用访问派生类定义的成员。
- virtual关键字其实质是告知编译系统,被指定为virtual的函数采用动态联编的形式编译。
- 虚函数的虚特征:基类指针指向派生类的对象时,通过该指针访问其虚函数将调用派生类的版本。
要点:
- 一旦将某个成员函数声明为虚函数后,它在继承体系中就永远为虚函数了。(派生类在定义时可以不加virtual关键字)
- 如果基类定义了虚函数,当通过基类指针或引用调用派生类对象时,将访问到它们实际所指对象中的虚函数版本。
- 只有通过基类对象的指针和引用访问派生类对象的虚函数时,才能体现虚函数的特性。
- 派生类中的虚函数要保持其虚特征,必须与基类虚函数的函数原型完全相同,否则就是普通的重载函数,与基类的虚函数无关。
- 派生类通过从基类继承的成员函数调用虚函数时,将访问到派生类中的版本。
- 只有类的非静态成员函数才能被定义为虚函数,类的构造函数和静态成员函数不能定义为虚函数。原因是虚函数在继承层次结构中才能够发生作用,而构造函数、静态成员是不能够被继承的。
- 内联函数也不能是虚函数(多态时)。因为内联函数采用的是静态联编的方式,而虚函数是在程序运行时才与具体函数动态绑定的,采用的是动态联编的方式,即使虚函数在类体内被定义,C++编译器也将它视为非内联函数。
- 类中定义的函数是内联函数,类中声明、类外定义且都没用inline关键字的是普通函数(注:inline要起作用,inline要与函数定义放在一起。inline是一种“用于实现的关键字,而不是用于声明的关键字)
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
inline virtual
唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如Base::who()
),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
- 基类析构函数几乎总是为虚析构函数。假定使用delete和一个指向派生类的基类指针来销毁派生类对象,如果基类析构函数不为虚,就如一个普通成员函数,delete函数调用的就是基类析构函数(而不是调用该派生类的析构函数)。在通过基类对象的引用或指针调用派生类对象时,将致使对象析构不彻底。(如果是虚析构函数,则会先调用派生类的析构函数再调用基类的析构函数)
纯虚函数和抽象类
纯虚函数:仅定义函数原型而不定义其实现的虚函数。
实用角度:占位手段place-holder。
方法学:接口定义手段,抽象表达手段。
1 | class X |
抽象类:包含一个或多个纯虚函数的类。
不能实例化抽象类,但可以定义抽象类的指针和引用。定义一个抽象类的派生类,必须定义所有纯虚函数,否则该派生类仍然是一个抽象类。
总结:
- 抽象类中含有纯虚函数,由于纯虚函数没有实现代码,所以不能建立抽象类的对象。
- 抽象类只能作为其他类的基类,可以通过抽象类对象的指针或引用访问到它的派生类对象,实现运行时的多态性。
- 如果派生类只是简单地继承了抽象类的纯虚函数,而没有重新定义基类的纯虚函数,则派生类也是一个抽象类。
运算符重载
运算符重载是C++的一项强大功能。通过重载,可以扩展C++运算符的功能,使它们能够操作用户自定义的数据类型,增加程序代码的直观性和可读性。
重载二元运算符
二元运算符的调用形式与解析:a@b 可解释成 a.operator@(b)
或解释成 operator@(a,b)
(@表示运算符)
如果两者都有定义,就按照重载解析:
1 | class X{ |
类运算符重载形式
非静态成员运算符重载:以类成员形式重载的运算符参数比实际参数少一个,第1个参数是以this指针隐式传递的。
1
2
3
4
5
6class Complex{
double real,image;
public:
Complex operator+(Complex b){……}//实际上是Complex+Complex,*this+b
......
};友元运算符重载:如果将运算符函数作为类的友元重载,它需要的参数个数就与运算符实际需要的参数个数相同。
1 | class Complex{ |
重载一元运算符
一元运算符:只需要一个运算参数,如取地址运算符(&)、负数(?)、自增加(++)等。
一元运算符常见调用形式如下,其中的@代表一元运算符,a代表操作数。
- 隐式调用形式:@a 或 a@ ,@a代表前缀一元运算,如“++a”;a@表示后缀运算,如“a++”。
- 显式调用一元运算符@:a.operator@()
一元运算符作为类成员函数重载时不需要参数,其形式如下:
1
2
3
4
5
6class X{
……
T operator@(){……};
}
//T是运算符@的返回类型。从形式上看,作为类成员函数重载的一元运算符没有参数
//但实际上它包含了一个隐含参数,即调用对象的this指针。前自增(减)与后自增(减):C++编译器可以通过在运算符函数参数表中是否插入关键字int 来区分这两种方式
1
2
3
4
5
6//前缀
operator ++ ();
operator ++ (X & x);//这里的X代表其他的类型,可能有其他操作
//后缀
operator ++ (int);
operator ++ (X & x, int);
重载赋值运算符=
当以拷贝的方式初始化一个对象时,会调用拷贝构造函数;当给一个对象赋值时,会调用重载过的赋值运算符。
即使我们没有显式的重载赋值运算符,编译器也会以默认地方式重载它。默认重载的赋值运算符功能很简单,就是将原有对象的所有成员变量一一赋值给新对象,这和默认拷贝构造函数的功能类似。
对于简单的类,默认的赋值运算符一般就够用了,我们也没有必要再显式地重载它。但是当类持有其它资源时,例如动态分配的内存、打开的文件、指向其他数据的指针、网络连接等,默认的赋值运算符就不能处理了,我们必须显式地重载它,这样才能将原有对象的所有数据都赋值给新对象。
1 |
|
- operator=() 的返回值类型为
Array &
,这样不但能够避免在返回数据时调用拷贝构造函数,还能够达到连续赋值的目的。下面的语句就是连续赋值:arr4 = arr3 = arr2 = arr1; if( this != &arr)
语句的作用是「判断是否是给同一个对象赋值」:如果是,那就什么也不做;如果不是,那就将原有对象的所有成员变量一一赋值给新对象,并为新对象重新分配内存。return *this
表示返回当前对象(新对象)。- operator=() 的形参类型为
const Array &
,这样不但能够避免在传参时调用拷贝构造函数,还能够同时接收 const 类型和非 const 类型的实参。 - 赋值运算符重载函数除了能有对象引用这样的参数之外,也能有其它参数。但是其它参数必须给出默认值,例如:
Array & operator=(const Array &arr, int a = 100);
重载赋值运算符[]
[ ]是一个二元运算符,其重载形式如下:
1
2
3
4class X{
……
X& operator[](int n);//调用:X[n]
};重载[]需要注意的问题
- []是一个二元运算符,其第1个参数是通过对象的this指针传递的,第2个参数代表数组的下标
- 由于[]既可以出现在赋值符“=”的左边,也可以出现在赋值符“=”的右边,所以重载运算符[]时常返回引用。(既能作为左值赋值也能作为右值读取,如果不是引用,作为左值时函数只是返回了一个临时对象,赋值写入没用意义)
- []只能被重载为类的非静态成员函数,不能被重载为友元和普通函数。
重载( )
运算符( )是函数调用运算符,也能被重载。且只能被重载为类的成员函数。
运算符( )的重载形式如下:
1
2
3
4class X{
……
X& operator( )(参数表);//其中的参数表可以包括任意多个参数。
};运算符( )的调用形式如下:
1
2
3
4
5X Obj; //对象定义
Obj()(参数表); //调用形式1
Obj(参数表); //调用形式2,普遍用这种形式
类强制转换的重载
- 类型转换函数没有参数。
- 类型转换函数没有返回类型。
- 类型转换函数必须返回将要转换成的type类型数据。
看一下实例便知:
1 | /* |
模板
模板(template)是C++实现代码重用机制的重要工具,是泛型技术(即与数据类型无关的通用程序设计技术)的基础。 模板是C++中相对较新的语言机制,它实现了与具体数据类型无关的通用算法程序设计,能够提高软件开发的效率,是程序代码复用的强有力工具。
模板概念:模板是对具有相同特性的函数或类的再抽象,模板是一种参数多态性的工具,可以为逻辑功能相同而类型不同的程序提供一种代码共享的机制。 一个模板并非一个实实在在的函数或类,仅仅是一个函数或类的描述,是参数化的函数和类。
函数模板
函数模板提供了一种通用的函数行为,该函数行为可以用多种不同的数据类型进行调用,编译器会据调用类型自动将它实例化为具体数据类型的函数代码,也就是说函数模板代表了一个函数家族。 与普通函数相比,函数模板中某些函数元素的数据类型是未确定的,这些元素的类型将在使用时被参数化;与重载函数相比,函数模板不需要程序员重复编写函数代码,它可以自动生成许多功能相同但参数和返回值类型不同的函数。当实例化一个函数模板时,编译器自动生成一份具有相应类型的代码。
函数模板的定义:
1 | template <class T1, class T2,…> |
template是定义模板的关键字;写在一对<>中的T1,T2,…是模板参数,其中的class表示其后的参数可以是任意类型。
注意事项 :
在定义模板时,不允许template语句与函数模板定义之间有任何其他语句。
1
2
3template <class T>
int x; //错误,不允许在此位置有任何语句
T min(T a,T b){…}函数模板可以有多个类型参数,但每个类型参数都必须用关键字class或typename限定。此外,模板参数中还可以出现确定类型参数,称为非类型参数(浮点数和类对象是不允许作为非类型模板参数的)。例:
1
2
3
4
5
6
7
8
9
10
11
12template <class T1,class T2,class T3,int T4>//在传递实参时,非类型参数T4只能使用常量
T1 fx(T1 a, T 2 b, T3 c){…}
//如:
template<int Val, typename T>
T addValue(T x)
{
return x + Val;
}
//调用,这种情况必须像类一样显式实例化
addValue<5,int>(1);不要把这里的class与类的声明关键字class混淆在一起,虽然它们由相同的字母组成,但含义是不同的。这里的class表示T是一个类型参数,可以是任何数据类型,如int、float、char等,或者用户定义的struct、enum或class等自定义数据类型。
为了区别类与模板参数中的类型关键字class,标准C++用typename作为模板参数的类型关键字,同时也支持使用class。比如,把min定义的template 写成下面的形式是完全等价的:
1
2template <typename T>
T min(T a,T b){…}
函数模板的实例化:
实例化发生的时机 模板实例化发生在调用模板函数时。当编译器遇到程序中对函数模板的调用时,它才会根据调用语句中实参的具体类型,确定模板参数的数据类型,并用此类型替换函数模板中的模板参数,生成能够处理该类型的函数代码,即模板函数。
当多次发生类型相同的参数调用时,只在第1次进行实例化。假设有下面的函数调用:
1
2
3
4
5
6int x=min(2,3);
int y=min(3,9);
int z=min(8.5);
//编译器只在第1次调用时生成模板函数,当之后遇到相同类型的参数调用时,不再生成其他模板函数,
//它将调用第1次实例化生成的模板函数。
实例化方式:
隐式实例化:编译器能够判断模板参数类型时,自动实例化函数模板为模板函数
1
2
3
4
5
6
7
8//隐式实例化,表面上是在调用模板,实际上是调用其实例
template <typename T>
T max (T, T);
…
int i = max (1, 2);
float f = max (1.0, 2.0);
char ch = max (‘a’, ‘A’);
…显示实例化explicit instantiation :编译器不能判断模板参数类型或常量值,需要使用特定数据类型实例化
1
2
3
4
5
6
7
8
9//语法形式:模板名称<数据类型,…,常量值,…> (参数)
template <class T>
T max (T, T);
…
int i = max (1, ‘2’); // error: data type can’t be deduced
int i = max<int> (1, ‘2’);//将'2‘强制转化为int类型
…
函数模板的特化(函数模板的特化,只能全特化):
特化的原因:在某些情况下,模板描述的通用算法不适合特定的场合(数据类型等) 比如:如max函数
1
2
3char * cp = max (“abcd”, “1234”);
实例化为:
char * max (char * a, char * b){return a > b ? a : b;}这肯定是有问题的,因为字符串的比较为:
1
2char * max (char * a, char * b)
{ return strcmp(a, b)>0 ? a : b; }因此需要写出一份特化版本的max函数,在遇到字符串时使用特化版本而不使用泛型版本。
所谓特化,就是针对模板不能处理的特殊数据类型,编写与模板同名的特殊函数专门处理这些数据类型。
模板特化的定义形式:
template <> 返回类型 函数名<特化的数据类型>(参数表) { …… }
说明: ① template < >是模板特化的关键字,< >中不需要任何内容; ② 函数名后的< >中是需要特化处理的数据类型,实际上,这是对泛型版本说明该函数要特化的形式(即显式告知泛型中的T)。
1 | //泛型版本 |
- 当程序中同时存在模板和它的特化时,特化将被优先调用;
- 在同一个程序中,除了函数模板和它的特化外,还可以有同名的普通函数。其区别在于C++会对普通函数的调用实参进行隐式的类型转换,但不会对模板函数及特化函数的参数进行任何形式的类型转换(需要匹配或者显式实例)。
- 当同一程序中具有模板与普通函数时,其匹配顺序(调用顺序)如下:
- 1.完全匹配的非模板函数
- 2.完全匹配的模板函数
- 3.类型相容的非模板函数
类模板
类模板可用来设计结构和成员函数完全相同,但所处理的数据类型不同的通用类。
类模板的声明:
1 | template<class T1,class T2,…> |
其中T1、T2是类型参数。类模板中可以有多个模板参数,包括类型参数和非类型参数
非类型参数是指某种具体的数据类型,在调用模板时只能为其提供用相应类型的常数值。非类型参数是受限制的,通常可以是整型、枚举型、对象或函数的引用,以及对象、函数或类成员的指针,但不允许用浮点型(或双精度型)、类对象或void作为非类型参数。
1 | template<class T1,class T2,int T3> |
类模板的成员函数的定义:
类内成员函数定义,与常规成员函数的定义类似,另外 “模板参数列表”引入的“类型标识符”直接作为数据类型使用,“模板参数列表”引入的“普通数据类型常量”直接作为常量使用。
在类模板外定义,语法:
template <模板参数列表> [返回值类型] [类模板名<模板参数名表>::] [成员函数名] ([参数列表]){…};
就比普通的模板函数多了
[类模板名<模板参数名表>::]
。
类可以特化,与函数模板不同的是,类不仅可以全特化,也可以偏特化。
偏特化是指提供另一份template定义式,而其本身仍为templatized
,这是针对于template
参数更进一步的条件限制所设计出来的一个特化版本。也就是如果这个模板有多个类型,那么只限定其中的一部分;
1 |
|
强调:
- 函数模板只有特化,没有偏特化;
- 模板、模板的特化和模板的偏特化都存在的情况下,编译器在编译阶段进行匹配,优先匹配特殊的(如能匹配全特化就不会匹配偏特化);
- 模板函数不能是虚函数;因为每个包含虚函数的类具有一个virtual table,包含该类的所有虚函数的地址,因此vtable的大小是确定的。模板只有被使用时才会被实例化,将其声明为虚函数会使vtable的大小不确定(函数模板可能用到,可能用不到)。所以,成员函数模板不能为虚函数。
- 编译器在编译一个类的时候,需要确定这个类的虚函数表的大小。一般来说,如果一个类有N个虚函数,它的虚函数表的大小就是N,如果按字节算的话那么就是4*N。 如果允许一个成员模板函数为虚函数的话,因为我们可以为该成员模板函数实例化出很多不同的版本,也就是可以实例化出很多不同版本的虚函数
- 那么编译器为了确定类的虚函数表的大小,就必须要知道我们一共为该成员模板函数实例化了多少个不同版本的虚函数。显然编译器需要查找所有的代码文件,才能够知道到底有几个虚函数,这对于多文件的项目来说,代价是非常高的,所以才规定成员模板函数不能够为虚函数。
异常处理
异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。
提供异常的基本目的就是为了处理上面的问题。基本思想是:让一个函数在发现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或者间接)调用者能够处理这个问题。也就是《C++ primer》中说的:将问题检测和问题处理相分离。
优点有以下几点:
- 异常处理可以在调用跳级。这是一个代码编写时的问题:假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理。而使用异常处理的栈展开机制,只需要在一处进行处理就可以了,不需要每级函数都处理。
- 栈展开:如果在一个函数内部抛出异常(throw),而此异常并未在该函数内部被捕捉(catch),就将导致该函数的运行在抛出异常处结束,所有已经分配在栈上的局部变量都要被释放。然后会接着向下线性的搜索函数调用栈,来寻找异常处理者,并且带有异常处理的函数(也就是有catch捕捉到)之前的所有实体(每级函数),都会从函数调用栈中删除。
- 栈展开危害:在栈展开的过程中,如果被释放的局部变量中有指针,而该指针在此前已经用new运算申请了空间,就有可能导致内存泄露。因为栈展开的时候并不会自动对指针变量执行delete(或delete[])操作。
- 整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。
- 整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。
异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw。
- throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
- catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
- try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:
1 | //如果 try 块在不同的情境下会抛出不同的异常, |
抛出异常
可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。
以下是尝试除以零时抛出异常的实例:
1 | double division(int a, int b) |
异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构。
析构的顺序与构造的顺序相反,这一过程称为栈的解旋(unwinding).
在try中抛出的异常被相应的catch捕获
在catch中抛出的异常可以被上层函数调用的catch捕获
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
using namespace std;
void fun(int x)
{
try
{
if(x == 0)
throw "yichang";
}
catch(...)
{
cout << "in fun" << endl;
throw 1; // throw to main
}
}
int main()
{
try
{
fun(0);
}
catch(int n)
{
cout << "in main, get it" <<endl;
}
return 0;
}
捕获异常
catch 块跟在 try 块后面,用于捕获异常。您可以指定想要捕捉的异常类型,这是由 catch 关键字后的括号内的异常声明决定的。
1 | try |
- catch的匹配过程是找最先匹配的,不是最佳匹配。
- catch的匹配过程中,对类型的要求比较严格。不允许标准算术转换和类类型的转换。(类类型的转化包括两种:通过构造函数的隐式类型转化和通过转化操作符的类型转化)。
实例
1 |
|
由于我们抛出了一个类型为 const char* 的异常,因此,当捕获该异常时,我们必须在 catch 块中使用 const char*。当上面的代码被编译和执行时,它会产生下列结果:Division by zero condition!
C++ 标准的异常
C++ 提供了一系列标准的异常,定义在<exception>
中,我们可以在程序中使用这些标准的异常。
定义新的异常
这部分看看就行,比较冷门且不常用。
- 建议自己的异常类要继承标准异常类。因为C++中可以抛出任何类型的异常,所以我们的异常类可以不继承自标准异常,但是这样可能会导致程序混乱,尤其是当我们多人协同开发时。
- 当继承标准异常类时,应该重载父类的what函数和虚析构函数。
- 因为栈展开的过程中,要复制异常类型,那么要根据你在类中添加的成员考虑是否提供自己的复制构造函数。
1 |
|
结果:
1 | MyException caught |
函数声明后面加throw()
C++函数后面加关键字throw(something)限制,是对这个函数的异常安全作出限制;这是一种异常规范,只会出现在声明函数时,表示这个函数可能抛出任何类型的异常。
- void fun() throw(); //表示fun函数不允许抛出任何异常,即fun函数是异常安全的,一旦异常,将是意想不到的严重错误。
- void fun() throw(…); //表示fun函数可以抛出任何形式的异常。
- void fun() throw(exceptionType); // 表示fun函数只能抛出exceptionType类型的异常。
例如:
- void GetTag() throw(int); // 表示只抛出int类型异常
- void GetTag() throw(int,char); // 表示抛出int,char类型异常
- void GetTag() throw(); // 表示不会抛出任何类型异常,一旦异常,将是意想不到的严重错误
- void GetTag() throw(…); // 表示抛出任何类型异常
void GetTag() throw(int); 表示只抛出int类型异常,并不表示一定会抛出异常,但是一旦抛出异常只会抛出int类型,如果抛出非int类型异常,调用unexsetpion()函数,退出程序。
来自C++之父Bjarne Stroustrup的建议
- 当局部的控制能够处理时,不要使用异常;
- 使用“资源分配即初始化”技术去管理资源;
- 尽量少用try-catch语句块,而是使用“资源分配即初始化”技术。
- 如果构造函数内发生错误,通过抛出异常来指明。
- 避免在析构函数中抛出异常。
- 保持普通程序代码和异常处理代码分开。
- 小心通过new分配的内存在发生异常时,可能造成内存泄露。
- 如果一个函数可能抛出某种异常,那么我们调用它时,就要假定它一定会抛出该异常,即要进行处理。
- 要记住,不是所有的异常都继承自exception类。
- 编写的供别人调用的程序库,不应该结束程序,而应该通过抛出异常,让调用者决定如何处理(因为调用者必须要处理抛出的异常)。
文件与流
文件流的分类
文件流是以外存文件未输入输出对象的数据流。输出文件流是从内存流向外存文件的数据,输入文件流是从外存文件流向内存的数据。每一个文件流都有一个内存缓冲区与之对应。
C++有三个用于文件操作的文件类:
1 | ofstream //文件的写操作(输出),主要是从内存写入存储设备(如磁盘),继承了istream类 |
想要使用文件流对文件进行操作,修必须要先定义它。
定义时须包含头文件#include< fstream >
1 |
|
打开文件
打开文件操作主要是把我们的文件流类对象和一个文件相关联起来,这样这个被打开的文件可以用类对象表示,之后我们对文件流类对象所做的输入和输出操作其实就是对这个文件所做的操作。
1 | void open(const char* filename,ios_base::openmode mode); |
其中mode
定义在所有IO
的基类中:即ios
类,它包括如下几种方式:
模式标志 | 描述 |
---|---|
ios::in | 读方式打开文件(ifstream对象默认方式) |
ios::out | 写方式打开文件 (ofstream对象默认) |
ios::trunc | 如果此文件已经存在, 就会打开文件之前把文件长度设置为0 |
ios::app | 尾部最加方式(在尾部写入) |
ios::ate | 文件打开后, 定位到文件尾 |
ios::binary | 二进制方式(默认是文本方式) |
ios::nocreate | 不建立文件,所以文件不存在时打开失败 |
ios::noreplace | 不覆盖文件,所以打开文件时如果文件存在失败 |
mode参数可以组合起来使用,但是两个参数之间必须要用操作符|隔开,如下
1 | ofstream out; //声明一个ofstream对象out |
判断文件是否打开成功
使用is_open()函数进行文件的判断
当成功打开文件返回真(true),失败返回假(false)
1 | fstream stream; |
关闭文件
当我们完成对文件的操作后,需要调用成员函数close
来关闭我们的文件流,close
函数的作用其实就是清空该类对象在缓存中的内容并且关闭该对象和文件的关联关系,然后该对象可以和其他文件进行关联。
1 | ofstream file; //声明一个ofstream对象file |
为了防止一个类对象被销毁后,还和某个文件保留关联关系,所以文件流类的析构函数都会自动调用close
函数。
读写文件
文本文件的读写
文本文件的读写很简单:用插入器(<<)向文件输出;用析取器(>>)从文件输入。
1 | 插入器(<<) 向流输出数据。比如说系统有一个默认的标准输出流(cout),一般情况下就是指的显示器,所以,cout<<“Write |
比如读取写入txt文件,如下
1 |
|
头文件<iomanip>
控 制 符 | 作 用 |
---|---|
dec | 设置整数为十进制 |
hex | 设置整数为十六进制 |
oct | 设置整数为八进制 |
setbase(n) | 设置整数为n进制(n=8,10,16) |
setfill(n) | 设置字符填充,c可以是字符常或字符变量 |
setprecision(n) | 设置浮点数的有效数字为n位 |
setw(n) | 设置字段宽度为n位 |
setiosflags(ios::fixed) | 设置浮点数以固定的小数位数显示 |
setiosflags(ios::scientific) | 设置浮点数以科学计数法表示 |
setiosflags(ios::left) | 输出左对齐 |
setiosflags(ios::right) | 输出右对齐 |
setiosflags(ios::skipws) | 忽略前导空格 |
setiosflags(ios::uppercase) | 在以科学计数法输出E与十六进制输出X以大写输出,否则小写。 |
setiosflags(ios::showpos) | 输出正数时显示”+”号 |
setiosflags(ios::showpoint) | 强制显示小数点 |
resetiosflags() | 终止已经设置的输出格式状态,在括号中应指定内容 |
1 | //对一个数操作时,先对输出流(ostream)进行格式化,最后再输出目标。 |
二进制文件的读写
二进制文件的操作需要在打开文件的时候指定打开方式为ios::binary
,并且还可以指定为既能输入又能输出的文件,我们通过成员函数 read
和 write
来读写二进制文件。
1 | //这里 buffer 是一块内存的地址,用来存储或读出数据。参数size 是一个整数值,表示要从缓存(buffer)中读出或写入的字符数。 |
eof()
infile.eof()判断读入文件是否达到文件尾部,若是则返回true。while(!infile.eof())就常常用来判断是否达到文件尾部,注意要在while循环体内不断地read,向下读,否则会死循环,因为eof()本身并不读取数据。
实例
1 |
|
文件位置指针
istream 和 ostream 都提供了用于重新定位文件位置指针的成员函数。这些成员函数包括关于 istream 的 seekg(”seek get”)和关于 ostream 的 seekp(”seek put”)。
seekg 和 seekp 的参数通常是一个长整型。第二个参数可以用于指定查找方向。查找方向可以是 ios::beg(默认的,从流的开头开始定位),也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)。
文件位置指针是一个整数值,指定了从文件的起始位置到指针所在位置的字节数。下面是关于定位 “get” 文件位置指针的实例:
1 | // 定位到 fileObject 的第 n 个字节(假设是 ios::beg) |
getline、get、gets和put函数
getline
由于流提取运算符(>>)会以空白符分割,所以我们的输入中无法包含空格。而使用getline函数可以指定分隔符,这样就可以读入包含空格的文本了(如:New York)。getline函数定义在头文件
1 | getline(ifstream& input, string s, char delimitChar)//getline接受的字符串长度不受限制 |
当函数读到分隔符或文件末尾时,就会停止。
get
get函数会从输入对象读取一个字符,而put函数会向输出对象写入一个字符。
get函数有三个版本:
1 | char get() //无参数的,返回从输入对象读取的一个字符。 |
用getline函数从输入流读字符时,遇到终止标志字符时结束,指针移到该终止标志字符之后,下一个getline函数将从该终止标志的下一个字符开始接着读入,如本程序运行结果所示那样。如果用cin.get()函数从输入流读字符时,遇终止标志字符时停止读取,指针不向后移动,仍然停留在原位置。下一次读取时仍从该终止标志字符开始。这是getline函数和get函数不同之处。
gets
引入cstdio头文件(#include
1 | char *gets(char *str) |
从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
如果成功,该函数返回 str。如果发生错误或者到达文件末尾时还未读取任何字符,则返回 NULL。
1 |
|
本函数可以无限读取,不会判断上限,所以程序员应该确保 buffer的空间足够大,以便在执行读操作时不发生溢出。如果溢出,多出来的字符将被写入到 堆栈中,这就 覆盖了堆栈原先的内容,破坏一个或多个不相关变量的值。这个事实导致gets函数只适用于玩具程序,为了避免这种情况,我们可以用fgets(stdin)
put
fstream 和 ofstream 类对象都可以调用 put() 方法。
当 fstream 和 ofstream 文件流对象调用 put() 方法时,该方法的功能就变成了向指定文件中写入单个字符。put() 方法的语法格式如下:
1 | ostream& put (char c);//c 用于指定要写入文件的字符。 |
1 |
|
cstring库常用函数
字符数组复制
strcpy
strcpy
的作用是复制整个字符数组到另一个字符数组,因此也就非常简洁,只有两个参数:
1 | //前一个参数是要复制到的目标数组起始位置,后一个是被复制的源数组起始位置。 |
strncpy
strncpy
与strcpy
很类似,只是可以指定复制多少个字符。它的原型是:
1 | //前两个参数的含义与strcpy相同,第三个参数num就是要复制的字符个数。 |
字符数组连接
strcat
strcat
的功能是把一个字符数组连接到另一个字符数组的后面。它的原型:
1 | //前一个是目标数组,后一个是要添加到后面的源数组。 |
strncat
指定字符数的拼接,原型是:
1 | char * strncat ( char * destination, const char * source, size_t num ); |
字符数组比较
strcmp
1 | //比较方式是:(字典序)两个字符串自左向右逐个字符相比(按ASCII值大小相比较),直到出现不同的字符或遇’\0’为止。 |
- str1>str2:则返回值>0;
- str1<str2:则返回值<0;
- str1=str2:则返回值=0;
strncmp
1 | int strncmp ( const char * str1, const char * str2, size_t num ) |
比较ptr1、ptr2指向的字符串,直到遇到不相同的字符或者空字符结束或者比较完前面的num bytes结束。如果都相同则返回0,如果第一个不同byte ptr1的小于ptr2的,返回负数,否则返回正数。
字符数组查找
strchr
strchr
函数可以在一个字符数组里找某个字符第一次出现的位置,如果未找到该字符则返回 NULL。它的原型是:
1 | //前一个是原字符数组,后一个是要查找的字符。 |
strstr
strstr
函数可以在一个字符数组里查找另一个字符数组第一次出现的位置。它的原型是:
1 | //前一个是文本串,后一个是模式串。 |
字符数组长度
strlen
strlen
用于求一个字符数组的长度,注意它是从给定的起始位置开始不断往后尝试,直到遇到’\0’为止的,因此它的时间复杂度并不是常数级别的,而是取决于字符数组的长度,在字符数组没有变动的情况下请务必不要重复调用。
1 | size_t strlen ( const char * str ); |
内存复制
memcpy
从source指向的地址拷贝num bytes到destination指向的地址。不检查source中的空字符,总是拷贝num bytes,可能产生溢出,当destination和source的大小小于num时。
1 | void* memcpy(void* destination,const void* source, size_t num) |
- str1 – 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
- str2 – 指向要复制的数据源,类型强制转换为 void* 指针。
- n – 要被复制的字节数。
- 该函数返回一个指向目标存储区 str1 的指针。
1 | // 将字符串复制到数组 dest 中 |
memmove
从source指向的地址拷贝num bytes到destination指向的地址。常用于同一字符串的改变。不检查source中的空字符,总是拷贝num bytes,可能产生溢出,当destination和source的大小小于num时。
1 | void* memmove(void* destination,const void* source, size_t num) |
在重叠内存块这方面,memmove() 是比 memcpy() 更安全的方法。如果目标区域和源区域有重叠的话,memmove() 能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中,复制后源区域的内容会被更改。如果目标区域与源区域没有重叠,则和 memcpy() 函数功能相同。
1 | 比如一个字符串123abc456,拷贝123abc到abc456的位置,当顺序覆盖时,abc先被覆盖成了123即123123456, |
1 | int main () |
内存比较
memcmp
1 | int memcmp ( const void * ptr1, const void * ptr2, size_t num ) |
比较ptr1、ptr2指向的内存块的前面num bytes,如果都相同则返回0,如果第一个不同byte ptr1的小于ptr2的,返回负数,否则返回正数。如果前面都相同,即使中间遇到空字符,也会继续比较下去,直到比较完所有的num bytes。
内存检索
memchr
1 | void * memchr ( void * ptr, int value, size_t num ); |
- ptr – 指向要执行搜索的内存块。
- value – 以 int 形式传递的值,但是函数在每次字节搜索时是使用该值的无符号字符形式。
- num – 要被分析的字节数。
在ptr指向的内存中的前num bytes中搜索值value,返回第一个value的指针,如果没有找到返回空指针。
内存设置
memset
1 | //设置ptr指向的内存的前面num bytes的值为value |
cmath库常用函数
数值
std::abs: 计算绝对值,包括整数类型;
std::fabs: 计算绝对值,不包括整数类型;
std::sqrt: 计算平方根;
std::cbrt: 计算立方根;
std::hypot: 计算两个数平方的和的平方根;
std::pow:幂运算;
std::exp: e^x;
std::exp2: 2^x;
std::log: ln(x);
std::log2: log2(x);
std::log10: log10(x);
std::fmod: 两数除法操作的余数(rounded towards zero);
std::remainder: 两数除法操作的余数(rounded to nearest);
std::remquo: 两数除法操作的余数;
取整
std::ceil: 不小于给定值的最近整数;
std::floor: 不大于给定值的最近整数;
std::trunc: 不大于给定值的最近整数;
std::modf: 将一个浮点数分解为整数及小数部分;
std::round: 舍入取整;
std::lround: 舍入取整, 返回long int;
std::llround: 舍入取整, 返回long long int;
角度
std::sin: 正弦;
std::asin: 反正弦;
std::cos: 余弦;
std::acos: 反正弦;
std::tan:正切;
std::atan:反正切;
std::atan2: 反正切;
std::sinh: 双曲正弦;
std::asinh: 双曲反正弦;
std::cosh: 双曲余弦;
std::acosh: 双曲反余弦;
std::tanh: 双曲正切;
std::atanh: 双曲反正切;
检测
std::nan: Generatequiet NaN;
std::isfinite: 检测是否是有限值;
std::isinf: 检测是否是无穷大值;
std::isnan: 检测是否是非数型;
std::isnormal: 检测是否是normal值,neitherinfinity, NaN, zero or subnormal;
std::signbit: 检测是否是负数;
std::isgreater: 检测第一个数是否大于第二个数;
std::isgreaterequal:检测第一个数是否大于或等于第二个数;
std::isless: 检测第一个数是否小于第二个数;
std::islessequal:检测第一个数是否小于或等于第二个数;
std::islessgreater:检测第一个数是否不等于第二个数;
std::isunordered:检测两个浮点数是否是无序的.
其他不常用
std::fma(x,y,z):x*y+z;
std::nearbyint: 使用当前的舍入模式取整(fegetround());
std::rint: 使用当前的舍入模式取整(fegetround());
std::lrint: 使用当前的舍入模式取整(fegetround()),返回long int;
std::llrint: 使用当前的舍入模式取整(fegetround()),返回long longint;
std::frexp: 将一个浮点数分解为有效数(significand)及以2为底的幂(x = significand* 2exp);
std::ldexp: x *2exp;
std::expm1: ex-1;
std::scalbn: x *FLT_RADIXn;
std::scalbln: x* FLT_RADIXn;
std::ilogb: 返回以FLT_RADIX为底,|x|的对数值,返回值为整数;
std::log1p: ln(1+x);
std::logb: 返回以FLT_RADIX为底,|x|的对数值,返回值为浮点数;
std::erf: 误差函数;
std::erfc: 互补(complementary)误差函数;
std::tgamma: 伽玛函数;
std::lgamma: log-伽玛函数;
std::copysign(x,y):返回x的值及y的正负符号组成的浮点数;
std::nextafter(x,y): 返回x之后y方向上的下一个可表示值;
std::nexttoward(x,y): 返回x之后y方向上的下一个可表示值;
std::fdim(x,y): Thefunction returns x-y if x>y, and zero otherwise;
std::fmax: 返回较大的值;
std::fmin: 返回较小的值;
std::fpclassify:为浮点值归类,返回一个类型为int的值;
例子
1 |
|