c++笔记
成员变量
class ss{
public:
int a;
void solve(){
a=10;
}
};
ss t;
ss p;
t.solve();p.solve();
对于类,上面是对类的一个声明,而下面是定义一个具体的对象,所以类中的int a
也同样是声明,只有具体定义的变量中才会有int a
,所以可以t.a=....
调用t中的a,所以这里的int a
是变量t
的,而这里的solve
函数是谁的,我们可能会下意识觉得是变量t
的,但其实他是类ss
的,是公共的,那么他既然是公共的,如果有多个变量调用solve
,它能正确的修改成员变量么,如何修改,答案是可以的,利用的是c++中的this
这个关键字,我们可以把上面这个solve函数用c语言中的指针实现
void solve(ss *t){
t->a=10;
}//与上面的效果一致
由此我们可以发现,只需要知道它具体的指针就可以准确作用到成员变量,所以原来类中的代码就可以用this改成
void solve(){
this->a=10;
}
可以发现与c语言实现的基本一致。
证明:
class ss{
public:
int a;
void solve(){
a=10;
printf("solve.a=%p\n",&a);
}
};
int main(){
ss t;
printf("t=%p\n",&t);
printf("t.a=%p\n",&(t.a));
t.solve();
return 0;
}
我们可以看出变量t
,t
中的a
,solve函数中的a
的地址都是相同的,由此可以说明函数的确自动确定了使用的成员变量的地址。
new&delete
new和delete对应了c中的malloc和free,new分配一块空间,delelte释放空间。
int *p=new int;
int *q=new int[10];//分配一个int的空间和分配10个int空间
delete p;
delete[] q;
我们可以发现,上面一共有两种形式的delete,一种是delete ..
,一种是delete[] ..
,通常就是第一种个体,第二种数组。每次申请完内存,都会有一个表来记录数值,[申请的内存大小|内存在堆中的起始位置],后面delete的依据就是根据这张表。
int *p=new int[10];
delete[] p;//释放成功
int *q=new int[10];
q++;//让q的地址发生偏移
delete[] q;//释放失败,因为在表中已经不存在
对于其中的第五条,具体是这样的情况,对于一个指针,他可能中间被new一个新空间,那么最后就要delete掉,但是也有可能不new,因为这个f函数不是一定调用的,所以就有这样的规则,delete空的时候不违规。
我们平时写程序可能都没有对我们所new的空间进行回收,但是这样并不会影响程序的运行,因为我们现在的操作系统都是多进程,每次都会重新分配空间,然后结束的时候全部回收,但这是一些简单的程序不会出问题,如果一些需要一直执行的程序,那么内存一旦泄露,将会造成严重的问题,所以要确保所有new的空间及时delete.
访问限制
类中一般有三种访问权限:
- private (只有此类中的函数可以访问)
- public (所有人都可以访问)
- protected (只有此类和子类可以访问)
其中对于private的一些例子
class ss{
private:
int a;
public:
ss():a(0){};
void solve(){
a++;
}
void pr(ss *p){
cout<<p->a<<endl;
}
};
int main(){
ss a;
ss b;
b.solve();
a.pr(&b);//调用pr函数传入b的地址,可以发现,在a中仍可以利用b的地址来调用b中的private,所以能否访问private不是看在哪里访问,而是看通过什么访问
return 0;
}
friend
还有一种权限,friend
,这个是在类声明的时候在类中将某些(函数,类,变量....)声明为友元,这样这些被声明的东西就可以访问该类中的private
。注意private只有在类声明的时候才可以声明,不能声明后才声明。
class ss{
private:
int a;
public:
ss():a(2){};
void solve(){
a++;
}
friend void pr(ss *p);//声明为友元函数,从而可以访问a
};
void pr(ss *p){
cout<<p->a<<endl;
}
class中未定义访问权限默认private,struct默认public。
初始化列表
- 定义
与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。
初始化列表分为两个阶段,初始化阶段和计算阶段。
-
初始化阶段,会执行构造函数对所有成员进行初始化。
-
计算阶段,根据具体构造函数对成员进行分配空间,赋值的操作。
实现方式
//类名(类型 x):变量名(x){};
class test{
private:
int a;
test(int x):a(x){};//将a初始化为x
};
初始化列表的作用
初始化列表对普通成员的初始化与直接构造函数内体内赋值初始化区别不大,但是对于类类型的变量,变量中的默认的构造函数如果无法调用,那么就需要利用初始化列表对它进行初始化,这时候就可以省去调用一次默认构造函数的过程,提高效率。
class Test1
{
public:
Test1(int a):i(a){}//默认构造函数带参数
int i;
};
class Test2
{
public:
Test1 test1 ;
Test2(Test1 &t1)
{test1 = t1 ;}//这里的test1不能调用默认的构造函数,所以无法初始化,所以直接赋值时错误的。
};
class Test2
{
public:
Test1 test1 ;
Test2(int x):test1(x){}//可以利用初始化列表来调用test1中的构造函数,实现对它的初始化
}
class Test2
{
public:
Test1 test1 ;
//Test2(int x):test1(x){}
//注意如果将上面这行删除的话也不会报错,注意这里的Test1 test1 并不会调用构造函数,因为这里只是构造一个类,里面的而这里的Test1 test1 只是声明的一个变量
//并不会调用构造函数,因为还没有构造,只是声明,而后面具体使用到test1的时候才需要调用。
}
继承
- 继承通常是将一些有关系的类联系起来,将他们归属一个类别。
- 一般会有一个基类用来被继承,通常也被称为父类,而继承父类内容的被称为子类
具体的继承关系:
其中person表示父类,student表示子类,是student一定是person,student包含person所有的特点,所以子类继承父类所有的内容。
父类和子类的关系:
构造函数:如果子类没有写构造函数,子类会调用父类的构造函数,如果父类没有默认构造函数,那么子类也会出问题。
class f{
public:
f(int x):a(x){};
private:
int a;
};
class s:f{
};
s t;//error,没有默认构造函数,要自己写一个构造函数
class s:f{
public:
s():f(5){};//调用父类的构造函数,调用父类构造函数只能在初始化列表里使用
};
隐藏:
class ss{
public:
void print(){cout<<"ss";}
void print(int a){cout<<a;}
};
class sp:public ss{
public:
void print(){//子类中的print函数与父类中的重名,会将父类的隐藏
cout<<"???"<<endl;
}
};
sp t;
t.print();//调用的子类的
t.print(2);//error,父类的print被隐藏了,会显示没有这个函数
t.ss::print();//accept,通过"::"指定定义的位置
函数重载和默认参数
-
重载:
重载就是函数名和返回类型相同,但是参数的数量或者类型不同。
void solve(int x,int y){//函数1
cout<<x<<endl;
}
void solve(string s,string p){//函数2
cout<<s+p<<endl;
}
solve(1,2);//调用函数1
solve("1","2");//调用函数2
-
默认参数
默认参数就是在函数声明时,给函数的参数一个默认值,在调用函数时,如果不赋新值,就调用默认值。
void solve(int x=5);
int main(){
solve(1);
solve();
//最后输出结果为1 5,说明第二个函数x为5,为默认值
}
void solve(int x){
cout<<x<<endl;
}
- 调用默认参数这个过程是发生在编译阶段,并不是让函数需要的参数减少,函数仍需要一个
int
类型的参数,只是编译阶段编译器给参数赋为默认值 - 并不建议使用这个默认参数,一方面是可读性差,因为读者看的时候会认为这个函数不需要参数,一方面肯能会发生一些错误
内联函数inline
-
用法:
inline void f(int x){//与正常函数相同,只是会有一个inline前缀
cout<<x;
}
int main(){
f(1);
}
inline函数与普通函数的区别就是,一个是调用函数,一个是将函数的代码替换。
上面的f函数可以转换为
int main(){
f(1);
cout<<1;//上面的函数替换为这一行
}
-
作用:
为了避免一些频繁调用小的函数而导致栈空间不足的情况,每个程序的栈空间是固定的,重复调用小函数就很浪费栈空间。
-
inline和define的区别
-
define:定义预编译时处理的宏,只是简单的字符串替换,无类型检查。
-
inline:关键字用来定义一个类的内联函数,引入它的主要原因是用它替代C中表达式形式的宏定义,编译阶段完成。
-
inline函数可以得到一定效率的提升,和c时代的宏函数相比,inline函数更加安全可靠,这个是以增加空间的消耗为代价的。
-
define是在代码处不加任何验证的简单替换;
-
而inline是将代码插入到调用处,会做参数类型检查。
-
替换方式
define字符串替换;inline是指嵌入代码,在编译过程中不单独产生代码,在调用函数的地方不是跳转,而是把代码 直接写到那里去,对于短小的函数比较实用,且安全可靠。
原文链接:https://blog.csdn.net/MyFamily_IT/article/details/78598080
- 类中给出定义的成员函数都是
inline
函数
class ss{
private:
int a;
public:
ss():a(1){};//是inline函数
void print(){//是inline函数
cout<<a;
}
void f();//不是inline函数
};
- inline只是对编译器建议,至于是否内联要取决于具体的函数,如果函数递归或者还是过大等,编译器就会拒绝
- 注意inline不是对函数的引用,是在编译阶段直接替换为函数体
- inline函数要声明和定义都是inline函数
假设现在又三个文件test.h
,test.cpp
,main.cpp
,通常都是test.h
中声明,test.cpp
中定义函数体,main.cpp
调用,但是对于inline函数 -
test.h
#include<iostream>
inline void solve(int x);
-
test.cpp
#include<iostream>
using namespace std;
inline void solve(int x){
cout<<x;
}
-
main.cpp
#include<iostream>
#include "test.h"
using namespace std;
int main(){
solve(1);
}
可以看到在test.h
中声明了solve
函数,并且是inline,然后我们运行main.cpp
发出现一下error
意思是inline函数solve
被使用,但是没有定义,那么为什么说没有定义,因为我们这里的solve是inline函数,并不是调用,所以它其实是把test.h
中的函数体替换过来,但是
test.h
中的solve没有函数体,所以就会说没有定义,所以就是说这里的test.cpp
其实没有用,我们只需要把test.cpp
中的定义写到test.h
中即可。
#include<iostream>
inline void solve(int x){
std::cout<<x;
};
const
- const用来修饰函数,变量,参数等
const int N=10;
这里const修饰了一个int
类型的变量
,注意是变量,被const修饰过后就无法直接修改,并不是常量
const具体用法:
- 用于全局变量的声明
void solve(){
extern const int sum;//这里将sum这个全局变量声明为const类型
sum++;//然后这里就会error,因为const不能修改
}
int sum=10;
- 指针
int const a=1000;
const int b=1000;//这两者等价 表示定义一个int类型的常数
int p=10;
const int *q1=&p;//表示无法用指针来改变指针所指向的值
int * const q2=&p;//表示指针指向的地址不变
//看const的位置,在*前表示指向的值不能改变,在后面表示指针的值不能变
- 函数中
//const int fun(const int a) const;函数中会出现的三个位置
int solve1(const int& a,const int& b){//参数传入常引用,这样不光可以提高效率,还可以防止引用的数据被意外修改
return a+b;
}
const int solve2(int a,int b){//返回值是一个常数
return a+b;
}
class hh{
private:
string str;
public:
hh(string s):str(s){};
void change1(string s){//编译通过
str=s;
}
void change2(string s) const{//编译失败,用const修饰过的成员还是可以防止修改成员变量
//str=s;error
}
};
class A{
int i;
public:
A():i(0){};
void f() const {cout<<"f() const"<<endl;}
void f(){cout<<"f()"<<endl;}
//上面这两个函数看起来返回类型和函数都相同,好像重复了,但其实并没有重复
//上面这两句可以具体为
//void f(const A* this) const....
//void f(A* this) ......
//所以这两个函数的参数实际上不同,构成重载
};
int main(){
const A a;
a.f();//输出f() const
}
- 函数里的这三种,其中类中修饰成员函数的,它的本质其实是对
this
const
- 关于const的地址的赋值
const int p=10;
int *q=&p;//error
int p1=10;
const int *q1=&p1;//accept
int p2=10;
int *q2=&p2;//accept
const int p3=10;
const int *q3=&p3;//accept
-
内存问题
char *s1="hello world";//前面加上const就没有warning,直接error
s1[0]='B';
har s2[]="hello world";
s2[0]='B';
引用
- 具体用法:
int x=10;
int &q=x;//可以通过改变q来改变x
const int &p=x;//不可以通过改变p来改变x
void f(int &x){
x++;
}
int main(){
int x=2;
f(x);
f(x*2);//error,x*2这个表达式有返回值,但是这个返回值没有存储的位置,并没有存储在一个具体的变量里
}
class sq{
public:
int x;
int& m_y;//声明一个引用
sq(int &p):x(1),m_y(p){}//类中声明的引用一定要在初始化列表中进行初始化
};
int x;
int & f(){
int q;
//return q; error,返回一个引用,这里的q是局部变量,函数结束后就会释放,呢个时候引用就没用了
return x;//ok
}
int g(int x);
int& g(int& x);//这两个函数不能同时出现,不构成重载
- 引用和指针的区别
- 引用不能为空 ,指针可以为空指针
- 引用是对象的一种别名,而指针是独立的对象
- 引用的地址不能改变,而指针可以改变
- 总的来说,引用有两种理解,一种是对象的别名,一种是
const
指针。
向上造型
- 对于子类而言,都可以当作父类来使用,这个具体的作用会在多态中体现
- 对于子类而言,子类包含父类所有的对象和函数,所以肯定可以当作父类来对待,只是把子类特有的忽略了而已
多态
- 关键字
virtual
- 当一个父类函数声明为
virtual
,那么他的子类的同名,参数相同的函数都默认为virtual
,但是还是建议加上virtual
,为了方便读者阅读
用法:
class ss{
private:
int x;
public:
virtual void solve(){
cout<<"111"<<endl;
}
};
class sp:public ss{
public:
virtual void solve(){
cout<<"222"<<endl;
}
};
int main()
{
ss a;
sp b;
ss *t=&a;
t->solve();//根据给的地址来判断具体调用那个solve()
t=&b;//这里就是上节说的向上造型
t->solve();
}
- virtual的作用,上面可以看出当调用solve时候,并不会直接决定调用那个solve,而是会到执行的时候再决定
virtual的实现
- 对于每个含有virtual函数的类而言,都会有一个虚表(vtable),这个表中包含这个类中所有的虚函数,然后这个类的对象,当生成这个类的对象时,会占用开始的一部分内存来存储一个指针,指针指向的就是虚表
- 关于virtual具体的作用方式
class ss{
public:
virtual void solve(){
cout<<"111"<<endl;
}
};
class sp:public ss{
public:
virtual void solve(){
cout<<"222"<<endl;
}
};
////
int main()
{
ss a;
sp b;
a=b;
a.solve();//输出111
··ss *l=&b;
l->solve();//222
//上面两个,通过"."调用其实还是a本身调用,赋值了并没有改变它指向vtable的指针
ss *t=&a;
t->solve();//111
int* p=(int*)&a;//这里取出指向vtable的指针
int* q=(int*)&b;
*p=*q;//改变指针的值
t->solve();//222
}
- 类中的析构函数最好是虚函数,为了防止内存泄漏
如图,其中Ellipse时shape的子类,如果析构函数不是动态的,那么delete p的时候会只调用shape的析构函数,而不是Ellipse的,就有可能导致内存的泄露
拷贝构造函数
class q{
private:
int x;
public:
q():x(1){cout<<"111"<<endl;}
};
int main()
{
q t;//调用构造函数,输出111
q t1=t;//并没有调用构造函数
}
上面这段代码我们可以看出,t1创建的时候并没有调用构造函数,那么就有几个问题,为什么没有调用构造函数,没有调用构造函数,会不会调用了别的函数
- 首先要分清楚两个概念,初始化和赋值
int a=1;//初始化
a=2;//赋值
所以对于类而言,类对象之间的复制,是对其中的成员进行复制,所以会有相应的函数,这种用于类的复制的函数叫做拷贝构造函数
- 调用拷贝构造函数的时机
- 函数传入参数是类对象
- 函数返回一个类对象
- 初始化的时候等于其他对象
class ss{
private:
int x;
public:
ss():x(1){cout<<"111"<<endl;}
ss(const ss &p){
cout<<"222"<<endl;
}
virtual ~ss(){
cout<<"333"<<endl;
}
};
ss f(){
ss p;
return p;//返回一个类对象,因为这个p是本地变量,所以要复制一份传回去
}
void l(ss t){//这里的参数是形参,要在这个函数区域复制一份
}
int main()
{
ss t;
ss p=t;//这样写等于ss p(t);会调用拷贝构造函数
f();
}
- 浅拷贝和深拷贝
一般情况都是浅拷贝,在静态分配空间的时候不会有什么问题,问题就在于如果动态分配空间的时候
class ss{
private:
char *name;
public:
ss(){
name=new char[10];
}
virtual ~ss(){
delete [] name;
}
};
int main(){
ss t;
ss p=t;//p浅拷贝了t,这个时候p,t指向了同一块内存,然后程序结束的时候,他们析构了两次同样的区域,就会出问题
}
- 深拷贝就是每次构造都单独分配空间,而不是简单的复制
静态成员/函数
- 关键字
static
- 静态局部变量
void solve(){
static int cnt=0;
cout<<++cnt<<" ";
}
int main()
{
for(int i=1;i<=10;i++) solve();
}
//最后会输出
//1 2 3 4 5 6 7 8 9 10
可以发现cnt
并没有清零,而是一直沿用上次,其实static
的本质就是全局变量,这里的cnt
会在solve
第一调用的时候初始化,但不会随着solve
的结束而释放,也就是会和全局变量一样一直存在,而这个变量其实是在全局变量区分配,所以它的内存在编译阶段就已经分配好了。
2. 全局变量
class ss{
};
ss a;
int main(){
}
这里的全局变量初始化调用构造函数的时机是什么时候?会在main函数执行之前,注意main函数只是一个函数,又编译器调用,但并不一定是一个调用,他会在之前完成很多事情。
全局变量之前可能会有依赖性,所以最好把有依赖的放到一块地方。
3. 类中的static
变量
class ss{
private:
static int x;//注意这里,是声明了一个全局变量,就像extern int x;
//静态变量可以通过this访问,比如 this->x;
public:
ss(){x=1;}//static类型不能用初始化列表
void solve(){}
};
int ss::x;//因为x在类中是声明,只是告诉编译器有这个东西,但并没有定义,所以要定义一下
// static int ss::x; 这是错误的,因为加上static就表示只能在这个文件中被访问,但是类中的静态成员是可能被外部访问的,产生冲突了
//int ss::x=10; 正确
int main(){
ss t;
t.x;
ss::x;//可以通过具体对象访问,也可以通过类来访问静态变量
}
- 类中静态成员函数
class A{
private:
int k;
static int i;
public:
A():k(1){i=2;}
void solve(){//非静态函数可以随意引用成员变量
cout<<k<<" "<<i<<endl;
}
static void f(){
cout<<k<<endl;//error 静态函数无法引用非静态变量,因为无法用this,目的是为了不创建具体对象也可以引用static函数,所以无法用this
cout<<i<<endl;
}
};
int A::i;
int main()
{
A t;
t.solve();
}
运算符重载
- 将c++中的一些运算符功能进行重载,关键字
operator
- 一下是可以重载的运算符
- 用法举例:
class ss{
private:
int x;
public:
ss(int n=0):x(n){}
const ss operator+(const ss& n) const{//成员函数的重载运算符,一个参数是this,一个是传入的参数,然后都要设为const,返回值如果不加const有可能会被当做左值
return ss(x+n.x);
}
void print(){
cout<<x<<endl;
}
};
int main()
{
ss t(1),p(5);
ss z;
z=t+p;//重载之后类对象之间可以直接相加
z=t+3;//这里是把3转化为了ss类型,因为ss的构造函数满足这样的转换
z=3+t;//error,这里是要把t转化为3,ss类型转换为int类型,但是没有相应的方法
z=3+3;//相加完然后转化为ss类型
}
上面的z=t+p
这里,这里是否调用的拷贝构造函数?
其实是没有,因为重载函数是成员函数,而在内部定义的成员还是都是inline
函数,所以他其实就是z=ss(t.x+p.x)
,所以没有调用拷贝构造函数
- 重载函数可以写成成员函数,也可以是非成员函数,那么具体怎么选择
一般单目运算符写成成员函数,有一些必须是成员函数
- 格式
- 赋值运算符
class sp{
private:
int x;
public:
sp():x(1){}
sp& operator=(const sp& t){//如果不写会有默认赋值函数,复制成员变量
if(this!=&t){//为了防止自己复制自己,就没有意义了
//赋值
}
return *this;
}
}
- 类型转换
class ss{
private:
int x;
public:
ss():x(1){}
ss(int p){}
//如果写成 explicit ss(int p){},那么下面就不会出现隐式转换
};
int main()
{
ss t=8;//这里是用ss(int p)这个构造函数将8转换为ss类型,然后再用赋值运算符
//上面加上explicit就不能这样写
t=(ss)8;//就要直接强制转换
return 0;
}
可以重写强制转换,从而实现强制转换成想要的类型
- 总的来说,最好不要用这种类型转换,尤其是隐式转换,因为我们可能只是打错了,并不想要它转换,这样隐式转换就容易带来很多问题,如果真的要转换的话,可以用显示的强制转换,这样更加只管,可读性也更好
模板(template)
- 用法:
namespace test1{
template<typename T1>//函数模板,这里的T1就是参数,一般会根据调用来隐式传递
T1 solve(T1 a){
cout<<"111"<<endl;
}
int solve(int a){
cout<<"Hello World"<<endl;
}
int a=solve(1);//当函数模板和普通函数重名,会优先调用普通函数
}
namespace test2{
template<typename T1>
T1 solve(T1 a,T1 b){
cout<<"test2"<<endl;
}
/*
int a=solve(1,'1');
这里的两个参数类型不一致,对应的T1就不唯一,所以会报错
*/
}
namespace test3{
template <class t>//泛化
class hh{
public:
void solve(){
cout<<"111"<<endl;
}
};
template<> class hh<int>{//特化
public:
void solve(){
cout<<"222"<<endl;
}
};
}
namespace test4{
template<typename t1,typename t2>//typename和class都可以
class hh{
};
template<typename t2> class hh<int,t2>{//偏特化,确定了其中一部分的类型
};
template<typename t1>//泛化
class ss{
};
template<typename t1> class ss<t1*>{//偏特化,这里的意思是,什么类型都可以,但它要是指针
};
}
class ss{
public:
typedef long long value_type;
};
namespace test5{
template<typename t1>
class hh{
public:
typename t1::value_type tmp;//这里的typename的意义,因为编译器在编译阶段还无法确定t1的类型,不知道t1::value_type这样的类型是否正确,所以就会报错,我们可以在前面加上typename来告诉编译器这是一个类型
hh(typename t1::value_type t):tmp(t){};
};
}
template<class t1>
class hh{
private:
t1 a;
public:
void solve(){
cout<<"Hello World"<<endl;
}
};
template<class t1>
void solve(t1 a,t1 b){
cout<<a+b<<endl;
}
int main(){
hh<int> tmp;//类模板一定要显式定义
tmp.solve();
hh<int>().solve();//也可以用这种临时对象的方法
solve(1,2);//函数模板可以隐式定义,会自动判断类型
long long sp=2;
test5::hh<ss> t(sp);
cout<<typeid(t).name()<<endl;
}
异常
- 异常处理提供了一种可以使程序从执行的某点将控制流和信息转移到与执行先前经过的某点相关联的处理代码的方法(换言之,异常处理将控制权沿调用栈向上转移)。
- 用法
try{//一般try中会运行一些函数,函数中可能会抛出异常,然后在catch中捕捉
}catch(){
}
void func() throw(int){//后面的throw(int)表示只能抛出int类型的异常
throw 0;//throw可以抛出任何东西
}
void f() throw();//表示不能抛出异常
void q() throw(int,long);//多种异常就用","隔开
void solve(){
try{
func();
}catch(const int& t){
cout<<"111"<<endl;
}catch(...){//这三个点表示接受所有异常,一般来说是最后的选项,最坏的打算
cout<<"222"<<endl;
}
}
- 在c中
malloc
时候如果内存不够会返回一个null,但是在c++中,new的内存不够会抛出一个异常 - 最好不要在构造函数中抛出异常,容易导致内存泄漏
class ss{
public:
ss(){//构造函数抛出异常导致构造函数没有完成,这样就不会调用析构函数,buf分配的内存就会浪费
buf=new char[1024];
delete this;
throw 0;
}
~ss(){delete buf;}
private:
char *buf;
}
流
- 概念:在c++中,数据的传入传出如同水流一样,是线性单向的,所以c++叫这个过程为"流",实现过程的类叫做"流类"
- 集中常用的类
- istream:常用于接收从键盘输入的数据;
- ostream:常用于将数据输出到屏幕上;
- ifstream:用于读取文件中的数据;
- ofstream:用于向文件中写入数据;
- iostream:继承自 istream 和 ostream 类,因为该类的功能兼两者于一身,既能用于输入,也能用于输出;
- fstream:兼 ifstream 和 ofstream 类功能于一身,既能读取文件中的数据,又能向文件中写入数据。
————————————————
原文链接:https://blog.csdn.net/ccc369639963/article/details/122905471
>>
和<<
>>
符号
可以看到第一个参数是istream&
类型的,不能加const
,因为可能要连续读入,就像上面一样,所以位置会一直改变
2. <<
符号
同样一个参数是ostream&
类型,这时候第二个参数要为const类型,防止在输出的时候改变