看c++primer知识点总结(基础部分)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
提示:本人自看c++primer的知识点总结
提示:以下是本篇文章正文内容,下面案例可供参考
一、变量和基本类型
1.声明
声明:使得名字为程序所知,一个文件如果想用到别处定义的名字,则必须包含对那个名字的声明。 定义:负责创建于名字关联的实体定义会申请存储空间,成为实体变量的定义必须出现且只能出现在一个文件中,二其他地方用到该变量的文件必须对器声明,绝不能重复定义
初始化 未初始化的变量含有一个不确定的值
规范 用户自定义的类名一般大写字母开头 变量名用小写字母 标识符由多单词组成,需要由明显区分,如student_loan
2.作用域
作用域大多以花括号分隔,main叫全局作用域,整个程序可以使用 声明a开始到main函数结束为止都能使用,出了main就无法使用叫块作用域 i定义于for语句内,只能在for语句里面使用
#include<iostream>
int main()
{
int a=0;
for(int i=0;i<a;i++);
}
3.const
const仅在当前文件内有效,可通过加extern在多文件内使用
引用
1.引用(变量的别名)不能用非常量引用指向一个常量对象 2.引用的类型和其引用的对象类型一致(有例外) 3.引用(指针也一样)里,一条声明语句由一个基本数据类型和后面的一个声明符(即变量名)
const int ci =1024;
int &r2 = ci;
因为ci是一个常量,不能赋值,所以不能用引用来改变ci的值,否则因为r2是int型,可以改变r2的值二改变ci的值 4.对const的引用叫常量引用,允许常量引用绑定非常量对象,字面值和一般表达式(*引用只能绑定在对象上,但常量引用能绑定字面值)
int i = 42;
int &r = i;
const int &r1 = i;
const int &r3 = r1*2;
int &k = 10;
const int &j =10;
可以通过i和r来修改i的值,但不允许修改r1,他是常量引用
5.指向常量的指针为底层const ,const int *pi; 6.常量指针和常量为顶层const , int *const pi;和const int pi;
7.常量表达式是指针不会改变并且在编译时就能得到计算结果的表达式
const int max_file = 20;
const int limit = max_file+1;
int staff_size = 27;
const int sz = get_size();
更新日期4.23
4.指向常量的指针(和引用规则一样)
const double pi = 3.14;
double *ptr = π
const double *pctr = π
*cptr = &42;
可以将double的常量给指向double类型的指针(此为,指针类型和其所指对象类型一致的例外)
double dval = 3.14;
cptr = &dval;
但不能通过cptr改变dval的值
指向常量的指针可指向一个非常量
5.常量指针(顶层const)
其不变的是本身的值,而非指向的那个值
int a = 0;
int *const curerr = &a;
curerr是先为常量再是指针,*在const前,从右往左看,curerr为常量再是指针 常量指针和指向常量的指针是不一样的,一个本身值不变,一个是指向对象的值不变
6.处理类型
类型别名是一个名字,是某种类型的同义词(我理解为把数据类型换名字) 传统:typedef cpp:用别名声明using SI = sale_item;,SI为sales——item的同义词
typedef char *pstring;
const pstring *ps;
const pstring cstr = 0;
这里不能把pstring理解为char *,把cstr看成const char *cstr(可以理解为const pstring,pstring为指针,即为常量指针)
decltype,选择并返回操作数的数据类型 仅得类型,不算出值
decltype(f()) sum = x;
7.自定义数据结构
用struct定义类 tip:类定义最后要加分号
二、字符串、向量和数组
1.using声明
std::cin是要使用命名空间std的名字cin 有了using声明,无需在程序里加前缀(如命名空间::)能使用命名空间中的成员
using namespace std;
2.标准库类型string
初始化string
使用等号(=)初始化一个变量,实际上执行拷贝初始化,把等号右侧的初始值拷贝到新创建的对象中,不适应等号执行直接初始化
srting s1 = s2;
string s1(s2);
srting操作(我只记录了一部分)
s.empty();
s.size;
getline(is,s);
输出时第一个字符到遇见下一处空白为止,使用getline能保留输入时的空白符,其一遇到换行符就结果读取操作冰返回结果(一开始是换行符,所得结果是个空) 更新日期4.24
string操作
比较运算符,对大小写敏感 1.若两个string对象长度不同,且较短的string对象的每个字符与较长的string对象的每个字符对应的每个字符对应位置上字符相同,则较短小于较长 2.若在某些对象位置不一样,则比较第一对相异字符结果
string str="hello";
string phrase="hello world";
string slang="hiya";
相加:左侧运算对象与右侧运算对象串接
string s1="hello,",s2="world\n";
string s3=s1+s2;
s1+=s2;
字面值与string对象相加 加法运算符两侧至少一个是string
string s3=s1+","+s2+'\n';
string s6=s1+","+world;
srting s7=("hello"+",")+s2;
3.范围for语句
范围for语句:为了对string对象中的字符操作(像字符数组一样的类) for(declaration(变量):expression(对象)){ } declaration定义一个变量,将被用于访问序列中的基础元素,每次迭代会被初始化为expression的下一个元素值
string str(“some string”);
for(auto c=str)
cout<<c<<endl;
改变字符 toupper函数:接收一个字符如何输出其对应的大写
string s("Hello World!");
for(auto &c:s)
c=toupper(c);
cout<<s<<endl;
只处理部分字符 使用下标运算符[ ]
string s("some string");
for(decltype(s.size()) index=0;index!=s.size()&&!isspace(s[indexi]); ++index)
s[index]=toupper(s[index]);
注:c++规定只有当左侧运算对象为真时才会检查右侧对象情况
结果为 SOME string
4.标准库类型vector
其表示对象的集合,也被称作容器
三种定义类型:
vector<int> ivec;
vector<sales_item> sales_vec;
vector<vector<string>> file;
初始化方法
vector<T> v1;
vector<T> v2(v1);
vector<T> v3(n,val);
vector<T> v4(n);
vector<T> v5={a,b,c···};
总结:圆括号内为构造vector对象,花括号为了列表初始化,成为元素初始化值,但也可能成为构造对象 如
vector<int> v1{10};
vector<string> v2{10};
为使vector高效快速地添加元素,在定义vector对象时设定其大小就没有必要了,直接定义一个空的vector对象即可 vector具有的操作:v.empty() 不存在任何元素返回为真,v.size()返回元素个数,v.push_back(t)在v尾端加一个值为t的元素,v.[n] *只能对确知已存在的元素执行下标操作,否则产生缓冲区溢出 缓冲区溢出:通过下标访问不存在的元素
更新日期5.10
5.迭代器
迭代器:为实现如下标运算符访问string对象的元素或vector对象的元素(类似指针)
auto b=v.begin(),e=v.end();
b表示第一个元素,e表示v尾元素的下一个位置
如果容器为空,则begin和end返回都是同一个迭代器,则都表示为尾后迭代器
string s("some string");
if(s.begin()!=s.end()){
auto it=s.begin();
*it=toupper(*it);
};
ps:养成使用迭代器和!=的习惯,就不用在意用了哪种类型的容器
但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素 如对于vector对象容器不能用push_back,会使迭代器失效(push_back在尾部添加元素)
vi为vector对象
auto mid=vi.begin()+vi.size()/2;
若vi有20个元素,则先vi.size()/2得10,再vi.begin()+10,即下标从0开始为vi[10]
用迭代器二分查找
auto beg=text.begin(),end=text.end();
auto mid=text.begin()+(end-beg)/2;
while (mid!=end&&*mid!=sought){
if(sought<*mid)
end=mid;
else
beg=mid+1;
mid=beg+(end-beg)/2;
}
上述代码,三个迭代器,beg:第一个元素,mid:指向中间的元素,end:指向尾元素的下一位置,在text的vector<string >中找sought 数组与vector相似,但大小无法改变,不能随意向数组增加元素,也无法拷贝和赋值 int(*parray)[10]=&arr;parray指向一个含有10个整数的数组,从内而外看,先parray是一个指向大小为10的数组的指针
6.数组和指针
因为由于数组会将数组类型的对象(数组名)是使用一个指向该数组首元素的指针 所以当使用数组作为一个auto变量的初始值时,会得到的类型是指针
int ia[]={0,1,2,3,4,5,6,7,8,9};
auto ia2(ia);
decltype(ia) ia3={0,1,2,3,4,5,6,7,8,9};
由于遍历要获取指向数组第一个元素的指针与指向数组尾元素的下一个元素的指针(与迭代器由于),获取尾后元素之后不存在的元素的地址
int arr[]={0,1,2,3,4,5,6,7,8,9};
int *p=arr;
int *e=&arr[10];
for(int *b=arr;b!=e;++b)
cout<<*b<<endl;
所以有了begin和end,只需直接使用这两个关键字即可,无需同上面一样麻烦了 更新日期5.12
7.c语言
很多情况下使用与数组的名字其实用的是指向数组首元素的指针 如
int ia[]={0,2,4,6,8};
此时ia[0]是一个使用数组名字的表达式,对数组执行下标运算,其实是对指向数组元素的指针执行下标运算
int *p=&ia[2];
int j=p[1];
int k=p[-2];
c库的string函数的头文件的c++版本为cstring
strlen(p) 返回p长度,空字符不算
strcmp(p1,p2) 比较相等性,p1==p2返回0;p1>p2返回正值;p1<p2返回负值
strcat(p1,p2) 将p2附加到p1之后,返回p1
strcpy(p1,p2) 将p2拷贝给p1,返回p1
对于标准库string对象,可直接通过关系,相等性运算符比较,但对于数组不行,因为其比较的是指针而非字符串,得加上*(解引用)或用strcmp函数 string对象s1,s2可直接连接起来,如string largestr=s1+“ ”+s2 而在c中,对于数组ca1,ca2只能通过strcat,strcpy函数, 如strcpy(largestr,ca1); strcat(largestr," “); strcat(largestr,” "); 来完成以上操作,且largestr的空间要足够大,否则会产生错误
混合string对象和c字符串
1.允许以空字符结束的字符数组来初始化string对象或赋值 2.在加法运算中允许字符数组为其中一个运算对象,复合赋值运算中以字符数组为右侧的运算对象,且反过来不成立
int int_arr[]={0,1,2,3,4,5};
vector<int> ivec(begin(int_arr),end(int_arr));
或者vector<int> subvec(int_arr+1,int_arr+4);
书p114 要使用范围for语句处理多维数组,除了最内层循环外,其他所有循环的控制变量都应该是引用类型,否则编译器初始化时,会把数组形式的元素转为指向该数组形式的元素内首元素的指针
int ia[3][4];
size_t cnt=0;
for (auto &row:ia)
for(auto &col:row){
col=cnt;
++cnt;
}
改变string对象中字符的值,必须把循环变量定义成引用类型
更新日期5.13
三、表达式
1.关系运算符(左结合律)
if(i<j<k)
即若k大于1or0则为真
先将i<j比较,并返回布尔值
if(i<j&&j<k)
这才是所需条件
2.赋值运算符
在比较运算时,除非比较类型为布尔值,否则不使用布尔字面值,即if(val==true)
若val为int型,用if(val==1)即可 赋值运算符(右结合律) 每个对象,它的类型或者与右边对象的类型相同,或者可由赋值语句中右边对象的类型转换得到
3.递增递减运算符
除非必须,否则不用其后置版本,如i++ 它是将原始值存储下来以便于返回这个未修改的内容,若我们无需使用修改前的值,则是一种浪费 后置递增运算符优先级高于解引用运算符,所以*pbeg++等价于 *(pbeg+1),先吧pbeg值加1,然后用pbeg的初始值作为其求值结果,解引用使用的是pbeg之前的值 解引用运算符优先级低于点运算符,所以要加括号
string s1="a string",*p=&s1;
auto n=(*p).size();
n=p->size();
否则 n=*p.size();
4.位运算符
~ 位求反(逐位按位求反) , << 左移 , >>右移动(移位时,移出边界外的位被舍弃掉),& 位与 , ^位异或,| 位或 移位运算符满足做结合律,是优先级低于算数运算符,高于关系、赋值、条件运算符
5.类型转化
类型转换主要是为了避免损失精度 隐式转换,编译器自动执行类型转换
int ival=3.541+3;
由于被初始化的对象类型无法改变,所有初始值被转换成改对象的类型
??在条件判断中,会将非布尔转换成布尔值 初始化时,初始值转成变量类型,赋值语句中,右侧运算对象转换成左侧运算对象的类型 算数转换,主要将运算对象转换成最宽的类型 整型提升,将小整数类型转换成较大的整数类型。把小于int型(如bool、char、signed char、unsigned char、short和unsigned short等)提升成int型
6.无符号类型的运算对象
当某个运算符的运算对象类型不一致时,都会将其转换成同一种类型但如果对象的类型时无符号类型则不一样了 首先先进行整型提升,如果一个运算对象时无符号类型、另一个运算对象时带符号类型,第一种情况,无符号类型>=带符号类型,那么带符号的运算对象转换成无符号的 第二种情况,带符号类型>带符号类型,如果无符号的值都能属于(存在)带符号中,转为带符号。如果不能,则转换成无符号类型的
显示转换
形式 cast-name (expression) cast-name:是static_cast(只要不包含底层const(常量)均可转换),dynamic_cast(很少用),const_cast(将常量对象转变为非常量),reinterpret_cast(将int* 变为插入*,但本质上仍是指向int的指针)中的一种 ?尽量避免使用强制转换
四、语句
复合语句:指用花括号括起来的语句,也称做块,一个块就是一个作用域。注:块不以分号为结束
1.条件语句分为:if else语句,switch语句
悬垂else:规定else与离他最近的,尚未匹配的if匹配 switch语句的case关键字和它对应值被称为case标签,case标签必须是整型常量表达式,不能是常量或者整数(我理解为必须既是整数也是常量,而不能是3.14(不是整数),或int ival=42(不是常量)这种),且任何两个case标签不能相同 当匹配成功时,会从当前标签开始执行往后所有case分支,直到结尾处,且除非有break语句 没有标签能匹配的的值话,则执行default标签 不允许跨过变量的初始化语句直接跳到改变量作用域内的另一个位置,如:
case ture:
string file_name=;
int ival=0;
int jval;
break;
case false:
jval=next_num();
所以c++语言规定,不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置,
但我们可以加上{},将声明语句位于语句块内部
2.迭代语句
迭代语句分为:while语句(不确定要迭代多少次用这个),for语句(for语句中定义的对象只在for循环体内可见),do while语句(不能在while中声明,然后在do中使用,因为时先执行do的),范围for语句(想对序列进行写操作,循环变量必须声明为引用类型) 范围for语句格式: for(变量:序列) 操作 注:1.序列必须是数组,vector或string等类型,用花括号括起来的初始列表 ,他们的特点有能返回的迭代器,如end、begin成员 2.不能对范围for语句增加vector对象元素,因为范围for语句中预存了end()的值,若序列中添加(或者其他容器)的元素,,使end()变得无效
3.跳转语句
跳转语句包括:break(只出现在迭代,switch,嵌套在此类循环的语句或块的语句中,范围是离最近的循环或switch) continue(只在for,while,do while,嵌套在此类循环的语句或块) goto(少用) return
4. try语句块和异常处理
异常:只存在于运行时的反常行为,行为超出了函数正常功能范围(包括失去数据库连接以及遇到意外输入) throw表达式:异常检测部分使用throw表达式来表达它遇到了无法处理的问题 try语句块:用来处理异常。以关键字try开始,以一个或多个catch子句结束,catch子句也被称作异常处理代码
五、函数
1.函数
函数定义:返回类型,函数名字,0个或多个形参组成的列表以及函数体 实参是形参的初始值,且实参类型与对应的参考类型匹配。 形参列表中的形参通常用逗号隔开,其中每个形参都是含有一个声明符的声明。 ?即使两个形参的类型一样,也必须把两个类型都写出来,形参不能同名
int f3(int v1,v2){};
int f4(int v1,int v2){};
tip:形参和实参类型基本一样,但实参能够隐式转换为形参,函数返回值类型由返回类型(返回首部定义的类型)决定的 函数的返回类型不能是数组,但可以是指向数组的指针
2.局部对象
形参和函数内部定义的变量统称为局部变量,仅在函数的作用域内可见 局部变量的生命周期:依赖于定义的方式 自动对象:只存在于块执行期间的对象 局部静态对象(static):在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止,才被销毁,在此期间即使对象所在的函数结束执行,也不会对它有影响 自动对象和局部静态对象的区别:自动对象在函数执行完毕后,本身空间也会被释放,但局部静态对象,函数结束执行后,其空间本身仍然保存
3.参数传递
传值参数:函数对形参做的所有操作不会影响实参 指针形参
void reset(int *ip)
{
*ip=0;
ip=0;
}
int i=42;
reset(&i);
cout<<"i="<<i<<endl;
传引用参数:引用形参能绑定初始化它的对象
void reset2(int &i)
{
i=0;
}
int j=42;
reset2(j);
cout<<"j="<<j<<endl;
上述过程中,形参i仅是j的又一个名字,在reset内部对i的使用即是对j的使用
使用引用避免拷贝 在拷贝大的类类型对象或者容器对象比较低效,甚至又的类类型不支持拷贝操作,函数便使用引用形参访问该类型对象 当函数无需修改引用形参的值时,最好使用常量引用,并将其最好声明为常量引用
const形参和实参
形参初始化与变量初始化一样 形参有顶层const时,传给它常量对象或非常量对象都可以
void fcn(const int i)
{
}
调用fcn函数可以传入const int也可以int,但反过来不行,即fcn(int *i) 不能传入const int *i
ps:尽量使用常量引用,若其他函数将他们形参定义成常量引用,则会无法调用
const int ci=42;
int i=ci;
??在引用和指针里面形参和实参对常量和非常量有要求
数组形参
数组两个性质:1、不允许拷贝数组 2、使用数组时会将其转换成指针,如果我们传给某个函数时一个数组,则实参自动地转换成指向数组首元素的指针。注:以数组为形参的函数也要确保不能越界
数组引用形参
形参时数组的引用
void print(int (&arr)[10])
{
for(auto elem:arr)
cout<<elem<<endl;
}
含有可变形参的函数
为了编写能处理不同数量实参的函数,若所有实参类型相同,可以传递一个名为initializer_list的标准库类型,1、和vector一样,也是一种模板类型2、不一样的是对象中的元素永远是常量值 编写输出错误信息的函数,可以作用于可变数量的实参
void error_msg(initializer_list<string> il)
{
for(auto beg=il;.begin();beg!=il.end();++beg)
cout<<*beg<<"";
cout<<endl;
}
if(expected!=actual)
error_msg({"functionX",expected,actual);
else
error_msg({"functionX","okay"});
两次调用传递参数数量不同,第一次传入3个值,第二次传入2个值
4.返回类型和return语句
若没返回值的,则使用void返回类型 如果想在它的中间位置提前退出,可以使用return语句
有返回值函数 每条return语句必须返回一个值,return语句返回类型与函数的返回类型相同,或能隐式地换成函数的返回类型,确保具有返回值的函数只能通过一条有效的return语句退出
不用返回局部对象的引用或指针 函数完成后它所占的存储空间也被释放掉,因此,局部变量的引用将不在指向有效的内存区域
调用运算符优先级与点运算符和箭头运算符相同,因此,如果函数返回指针,引用或类的对象,我们就能使用函数 调用的结果访问 结果对象的成员
如
const string &shorterstring(const string&s1,const string &s2)
{
return s1.size()<=s2.size()?s1:s2;
}
auto s2=shorterstring(s1,s2).size();
返回有值函数
引用返回左值
函数的返回类型决定函数调用是否为左值 只有返回引用的函数得到左值,别的均为右值, ??是可以像其他左值那样来使用返回值引用的函数调用,特别是返回类型是非常量引用的函数的结果赋值
如
char &get_val(string &str,string::size_type ix)
{
return str[ix];
}
int main()
{
string s("a value");
cout<<s<<endl;
get_val(s,0)='A';
cout<<s<<endl;
}
返回类类型是常量引用,则不能给结果赋值
列表初始化返回值
函数可以返回花括号包围的值的列表,如果列表为空,临时量执行值初始化(按类对象中元素的类型决定初始化),否则返回的值由函数返回类型决定
如果函数返回的是内置类型(是指int unsigned ,int 这些),则花括号包围的列表最多含一个值,而且该值所站空间不大于目标类型空间,如果返回的是类类型,则由类本身定义初始值
返回一个vector对象,存放表示错误信息的string对象
vector<string> progress()
{
if(expected.empty())
return { };
else if(expected==actual)
return {"functionX","okay"};
else
return{"functionX",expected,actual};
}
main函数允许没有return语句直接结束,因为编译器会自动添加 main函数不能调用它自己
返回数组指针
1.虽然函数不能返回数组,但可以使用类型别名
typedef int arr[10];
uesing arrT=int[10];
例子:type(*function(parameter_list))[demension]为声明一个返回数组指针的函数,如int(*func(int i))[10]; 可理解为func函数要一个int型实参,解引用func函数得到一个为10的数组,且类型为int 2.在已知函数返回指针指向的数组,可用decltype
decltype(odd) *arrptr(int i);
odd为数组且与函数返回类型一致
注:decltype不负责把数组类型转换成对应指针,所以要加一个*符号
3.尾置返回类型
auto func(int i)->int (*)[10];
返回一个指针,指向含10给整数的数组
5.函数重载
函数重载
同一个作用域内,几个函数名字相同但形参列表不同,成为重在函数,编译器会根据传递的实参类型判断想要的是哪个函数 一个拥有顶层的形参无法和另一方没有顶层const的形参区分开来
比如:
Record lookup(const Phone);
Record lookup(Phone);
若形参是某类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象,可以实现重载,此时const是底层
比如:
Record lookup(const Account&);
Record lookup(Account&);
重载与作用域
如果我们在内层作用中声明名字,它将隐藏外层作用域中声明的同名实体
string read();
void print (const string&);
void print (double);
void fooBar(int ival);
{
bool read=false;
string s=read();
void print(int);
print("Value:");
print(ival);
print(3.14);
}
调用print函数时,会先寻找声明,若找到了则会忽略外层作用域中的同名实体
正确的
void print (const string&);
void print (double);
void print(int);
void fooBar(int ival);
{
print("Value:");
print(ival);
print(3.14);
}
特殊用途语言特性
默认实参:在函数很多次调用中,它们都被赋予一个相同的值
用string对象表示窗口
typedef string::size_type sz;
string screen(sz ht=24,sz wid=80,char backgrnd='');
对于window=screen('?');
因为char型的?可以转为string::size_type
给定作用域中一个形参只能被赋予一次默认实参(一般在头文件中) 内联函数:多用于优化规模小,流程直接,频繁调用的函数,只需在返回类型前加个inline
constexpr函数
constexpr函数(可以由编译器去验证) 常量表达式constexpr是指值不会改变且在编译过程中就能够得到计算结果的表达式,能在编译时求值的表达式 constexpr,一般来说如果你认定变量是一个常量表达式(字面值,初始化的const对象),那把它声明成constexpr类型 constexpr函数要求,返回类型及所有形参类型是字面值类型且有且只有一条return语句 注:constexpr不一定返回常量表达式
函数匹配
1.首先调用对用的重载函数集,称候选函数 2.按提供的实参,从上面选出能被实参调用的函数,称可行函数 特征:1.形参与实参数量相等 2.每个实参与对应形参类型相同,或者能转换成形参类型 3.从可行函数选本次调用最匹配的函数,即实参类型与形参类型越接近越好。
函数指针
使用函数指针
函数指针:指向的是函数而非对象
bool lengthcompare(const sreing&,const string&);
只需将函数名替换成指针即可
bool (*pf) (const string&,const string&);
注:两端的括号不可少,若不写的话,p+返回值是bool的指针函数
把函数名作为值时,函数自动转换成指针
pf=&lengthcompare;
重载函数的指针
使用重载函数时,上下文要清晰地界定选哪个函数
void ff(int*);
void ff(unsigned int);
void (*pf1) (unsigned int)=ff;
(unsigned int)用于确定指向哪个函数
void (*pf2)(int*)=ff;
double (*pf3) (int*)=ff;
函数指针形参
形参可以是指向函数的指针
void useBigger(const string &s1,const string &s2,bool pf(const string &,const string &));
可以把函数作为**实参**使用,它会自动转换成指针
useBigger(s1,s2,lengthcompare);
能用类型别名和decltype来简化代码
typedef bool(*FunP)(const string &,const string &);
typedef decltype(lengthcompare) *FunP2;
FuncP和FuncP2是指针类型。因为decltype的结果是函数类型,只有在结果前加上*才能得到指针
void useBigger(const string &,const string &,FuncP2);
返回指向函数的指针
直接声明:int (f1(int i))(int,int); f1是函数名,有个*所以返回是指针,因为指针本身含有形参列表,所以此指针指向函数
六、类
类的基本思想是数据抽象和封装
数据抽象:是一种依赖于接口和实现分离的编程技术 接口:包括用户所能执行的操作 实现:包括类的数据成员,负责接口实现的函数体以及定义类所需的各种私有函数
封装:实现类的接口和实现的分离,封装后的类隐藏了实现的细节
实现一个对出售图书进行统计的一个程序
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using namespace std;
using std::string;
struct Sales_data {
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
double avg_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue;
};
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
double Sales_data::avg_price() const {
if (units_sold)
return revenue / units_sold;
else
return 0;
}
Sales_data& Sales_data::combine(const Sales_data& rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
istream &read(istream &is, Sales_data &item) {
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
ostream &print(ostream &os, const Sales_data &item) {
os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price();
return os;
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
int main()
{
Sales_data total;
if (read(cin, total)) {
Sales_data trans;
while (read(cin, trans)) {
if (total.isbn() == trans.isbn())
total.combine(trans);
else {
print(cout, total) << endl;
total = trans;
}
}
print(cout, total) << endl;
}
else {
cerr << "No data?!" << endl;
}
}
上面是完整代码,但仅仅包括了类的基本知识
1.引入this
std::string isbn() const{return bookNo} 对isbn成员函数的调用 total.isbn(). 当我们调用成员函数时,实际上再替某个对象调用它(可理解为再调用.isbn()时,对象是total) 成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象,就是total的地址传给了this,可等价认为Sale_data::isbn&total()//伪代码 this是一个常量指针,不允许改变this中保存的地址
引入const函数
默认情况下,this的类型是指向类类型非常量版本的常量指针(比如:Sale_data *const this,可理解为指针是常量,但其所指的对象为非常量)这会导致我们无法把this绑到一个常量对象上 所以在把const放在参数列表后,this便是个const Sale_data *const this 像上式这样使用const的成员函数成为常量成员函数 ??常量对象以及常量对象的引用或指针都只能调用常量成员函数,不能改变对象的内容
注:编译器分两步处理类:首先编译成员声明,然后才轮到成员函数体,所以成员函数体可随意使用类中的其他成员,无需在意出现次序
返回this对象的函数
Sales_data& Sales_data::combine(const Sales_data& rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
调用时 total.combine(trans);
total的地址在this参数上,而rhs绑定在trans上
2.定义类相关的非成员函数
类作者常需要定义一些辅助函数,如add,read,print这些函数定义的操作,从概念上属于类的接口组成部分,但实际上并不属于类本身 一般来说非成员函数时类接口的组成部分,则这些函数的声明应该与类在同一个头文件中
3.构造函数
类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫构造函数,其任务是初始化类对象的数据成员 构造函数的名字和类名相同,但没用返回类型,有一个参数列表和一个函数体(两者均可能为空),类可包含多个构造函数,不同的构造函数之间必须在参数数量或参数类型上有所区别
合成的默认构造函数
类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数(无需任何实参) 我们类没显示地定义构造函数,那么编译器会为我们隐式地定义一个默认构造函数,编译器创建的构造函数又称合成的默认构造函数 ?普通的类必须定义它自己的默认构造函数 原因:一、编译器只有在发现不包含任何构造函数的情况下,才会替我们生成一个默认的构造函数 二、对某些类来说,合成的默认构造函数可能执行错误操作(若类包含有内置类型或复合类型成员,只有全都在类内初始化,这个类才适合用合成的默认构造函数) 三、有时编译器不能为某些类合成默认的构造函数,如类中包含一个其他类类型成员且这个成员类型没有默认构造函数,则无法初始化
3.访问控制与封装
避免用户直达类的内部并控制它具体细节,则需封装public说明符之后的成员在整体程序内可被访问,public成员定义类接口 private说明符之后的成员可被类成员函数访问,但不能被使用该类的代码访问,封装了(隐藏)类的实现细节 class和struct最直观的区分方法是class含有private,而struct全是public,即默认访问权限的区别
4.友元
类允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元 //代码// 友元声明只能出现在类定义的内部,一般在开头或者结尾声明
5.类其他特性
类成员
定义一个类型成员 类可以自定义某种类型在类中的别名
public:
typedef std::string::size_type pos;
private:
pos cursor=0;
pos height=0,width=0;
std::string contents;
用来定义类型的成员必须先定义后使用
Screen类的成员函数
class Screen{
public:
typedef std::string::size_type pos;
Screen()=default;
Screen(pos ht,pos wd,char c):height(ht),width(wd),contents(ht*wd,c){}
char get() const
{ return contents[cursor];}
inline char get(pos ht,pos wd) const;
Screen &move(pos r,pos c);
private:
pos cursor=0;
pos height=0,width=0;
std::string contents;
};
一个构造函数令用户能定义屏幕的尺寸和内容,和其他两个成员为负责移动光标和读取给定位置的字符 类中小规模函数适合于被声明为内联函数
inline
Screen &Screen::move(pos r,pos c)
{
pos row=r*width;
cursor=row+c;
return *this;
}
char Screen::get(pos r,pos c) const
{
pos row=r*width;
return contents[row+c];
}
以上的函数中,get为重载成员函数,编译器根据实参的数量来决定运行哪个版本的函数 类数据成员初始值 默认情况下,我们希望window_mgr类开始时总有一个继续定义一个窗口管理类并用它表示显示器上三组Screen **默认初始化的Screen
class window_mgr{
private:
std::vector<Screen> screens{Screen{24,80,''}};
?当我们提供一个类内初始值时,必须以符号:或花括号表示
6.返回*this的函数
对于move的返回值是调用set的对象的引用 返回引用的函数是左值的,意味着函数返回的是对象本身,若我们返回Screen而非Screen&,则move返回值是*this的副本,只会改变临时副本
基于const的重载
注:一个const成员函数如果以引用的形式返回*this,那么它返回类型将是常量引用 通过区分成员是否const的,我们可以对其进行重载
class Screen{
public:
Screen &display(std::ostream &os)
{do_display(os);return *this;}
const Screen &display(std::ostream &os) const
{do_display(os);return *this;}
private:
void do_display(std::ostream &os) const(os<<contents;}
};
常量对象上调用const成员函数,非常量对象虽然能调用常量或非常量版本,但非常量版本是更好的匹配
第一个函数this指向一个非常量对象,因此返回一个非常量的引用
第二哥函数const成员则返回一个常量引用
7.类类型
每个类定义了唯一的类型,即使两个类的成员列表完全一致,他们也是不同的类型
类的声明
像把函数的声明和定义分离开来一样,仅声明类而暂时不定义它这种声明称前向声明。在声明之后定义之前是一个不完全类型
8.友元再探
如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员,友元类得放在类的开头 注:友元关系不存在传递性 除了令整个类作为友元之外,还可以仅为类中某个成员函数提供访问权限,明确指出那个类的哪个成员函数声明成友元
重载函数名字相同,但仍是不同的函数,如果一个类想把一组重载函数声明成它的友元,则需对这组函数中的每一个分别声明 即使我们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的
struct X{
firend void f() {
X() {f();}
void g();
void h();
};
void X::g() {return f();}
void X::f();
void X::h() {return f();}
9.类的作用域
一个类就是一个作用域,在类的外部成员名字被隐藏起来了,一旦遇到类名,定义的剩余部分(包括参数列表和函数体)在类的作用域之内。结果是我们可以直接使用类的其他成员而无须再次授权 因为返回类型通常出现在函数名之前,当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外,这时返回类型必须指明它时哪个类的成员,即类名::成员名 类名:: 成员名(参数列表){函数体}
名字查找与类的作用域
名字查找(寻找与所用名字最匹配的声明的过程)过程 先在名字所在的块中寻找其声明语句,只考虑在名字使用之前出现的声明 若没找到,继续查找外层作用域 若最终没用找到匹配的声明,则程序报错
类的定义: 首先编译成员的声明,直到类全部可见后才编译函数体 注:编译器处理完类中的全部声明后,才会处理成员函数的定义,所以成员函数体能使用类中定义的任何名字 如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找
typedef double Money;
string bal;
class Account{
public:
Money balance(){return bal;}
private:
Money bal;
};
在balance函数声明语句,编译器现在Account类的范围寻找Money声明,因为没用匹配成员,则在Account的外层作用域中查找,然后找到typedef语句。因为balance函数体在整个类可见后才被处理,因此它的return语句返回名为bal的成员,类型名的定义通常出现在类的开始处,这样就能确保所以使用该类型的成员都出现在类名定义之后
成员定义中的普通块查找
成员函数中: 首先在成员函数内查找该名字的声明(只在函数使用之前出现的声明才被考虑) 成员函数内没找到,则在类内查找,所以类成员可以被考虑。若类内也没用该名字的声明,则在成员函数定义之前的作用域内查找 不建议使用其他成员作为某个成员函数的参数
10.构造函数再探
如果成员时const,引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始列表为这些成员提供初值
class constRef{
public:
constRef(int ii);
private:
int i;
const int ci;
int &ri;
};
该构造函数正确形式是constRef::constRef(int ii)::i(ii),ci(ii),ri(ii){}
成员初始化顺序
成员初始化顺序与他们在类定义中的出现顺序一致 在一个成员是用另一个成员来初始化,则顺序很重要
class X{
int i;
int j;
public:
x(int val):j(val),i(j){}
};
实际上i先被初始化,用未定义的值j初始化i
?最好令构造函数初始值顺序与成员声明的顺序保持一致
11.隐式的类类型
转换构造函数:如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制 能通过一个实参调用的构造函数定义一条从构造函数的实参类型向类类型隐式转换的规则
string null.book+"9-999-99999-9";
string实参调用了Sales_data的combine的成员新生成的这个(临时)Scales_data对象被传给combine
编译器只会自动地执行一步类型转换
item.combine("9-999-99999-9");
item.combine(string("9-999-99999-9"));
item.combine(Sales_data("9-999-99999-9"));
item.combine(cin);
这句代码把cin转换成Sales_data,接受一个istream的Sales_data构造函数,通过读取标准输入创建了一个(临时的)Sales_data对象,Sales_data对象是个临时量,一旦combine完,我们就不能再访问它
抑制构造函数定义的隐式转换
可通过将构造函数声明为explicit加以阻止
class Sales_data {
public:
Sales_data()=default;
explicit Sales_data(const std::string &s):bookNo(s){}
explicit Sales_data(std::istream &);
}
关键字explicit只对一个实参的构造函数有效,要多个实参的构造函数不能用于执行隐式转换 只能在类内声明构造函数时使用explicit关键字,在类外部定义时无法使用
explicit Sales_data::Sales_data(istream &is)
{
read(is,*this);
当我们用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用Sales_data item1(null.book);//对的。无法用于拷贝初始化(使用=),而且编译器将不会在自动转换中使用该构造函数
12.聚合类
聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式,条件1.所以成员都是piblic的,没用定义任何构造函数,没有类内初始值,没有基类,也没有virtual函数 如
struct Data{
int ival;
string s;
}
Data val1={0,"Anna"};
字面值常量
条件:1.数据成员都必须时字面值类型 2.类必须至少会有一个constexpr构造函数 3.如果数据成员含类内初始值,则内置类型成员初始值必须是一条常量表达式,如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
类必须使用析构函数的默认定义,该成员负责销毁类的对象 constexpr构造函数声明成=default形式,否则既符合构造函数的要求(不能包含返回语句),又符合constexpr函数要求(意味着它能用于的唯一可执行语句时返回语句),综合两点,则cosntexpr构造函数体一般来说应该时空的
类的静态成员
声明
在成员的声明之前加上关键字static使得其与类关联在一起,定义一个类表示银行账户记录
class Account{
public:
void calculate() {amount+=amount*interestRate;}
static double rate() {return interestRate;}
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
类的静态成员存在与任何对象之外,外象中不包含任何与静态数据成员有关的数据,因此每个Account对象,包含两个数据成员owner和amount,只存在一个interestRate对象,且被所以Account对象共享
静态成员函数不能声明成const,也不能在函数体内使用this指针,(也不包含this指针)
使用
1.虽然静态成员不属于类的某个对象,但我们仍可以使用类的对象,引用,或指针来访问静态成员2.成员函数不用通过作用域运算符就能直接使用静态成员
定义
在类外部定义静态成员时,必须指明成员所属的类名,static关键字只能出现在类内部声明中
void Account::rate (double newRate)
{intersetRate=newRate;}
静态数据成员一旦被定义,则存在程序的整个生命周期中
double Account::interestRate=iniRate();//定义并初始化一个静态成员
从类名开始,定义语句的剩余部分都在类作用域之内了,则可使用initRate函数
总结
这是我第一次写这么多字的博客,主要都是我看完c++基础部分的觉得比较重要的部分,可能其中还有许多的问题,但感谢大家看完
|