C++基础内容复习

陌路散爱 提交于 2020-01-26 09:50:31

下列语句定义了5个变量:

int count;

double sales_price,sum;

std::string title;

Sales_item bookItem;

每个定义都是以类型说明符开始,如上int、double、std::string、Sales_item都是类型名,其中int和doubleshi是内置类型,std::string是标准库定义的类型(需要#include <string>), Sales_item是自定义类型

::作用域操作符,含义是右操作符的名字可以在左操作数的作用域中找到

简化std::string 的写法是使用using。

比如使用using std::string; using std:cin;之后,在下面的代码中就可以直接使用string和cin了。 

当进行string对象和字符串字面量混合连接操作时,+操作符的左右操作数必须至少有一个是string类型的。

比如,如下s1的初始化时正确的

       string item ="what a mess!";

       string s1 =item +",";

但是:string s1 ="what a mess!" +",";

就会报错,这个和C#是完全不一样的。

 

虽然任何整数数值都可以作为索引,但是索引的实际数据类型却是unsigned类型的string::size_type

vector

vector是一个类模版(class template),是同一种类型的对象的集合,每个对象都有一个对应的整数索引值,功效上可以参考C#的IList<T>.例如:

vector<int> int_list;

vector的初始化有如下几种方式:

vector<T> v1;//vector保存类型为T对象,默认构造函数v1为空

vector<T> v2(v1);//v2是v1的一个副本

vector<T> v3(n,i);//v3包含n个值为i的元素

vector<T> v4(n);//v4含有值初始化元素的n个副本

往队尾插入元素的函数是push_back

迭代器:

每种容器类型都定义了一个自己的迭代器,比如:

vector<int>::iterator iter;

每种容器都定义了begin和end函数;

比如可以这样写:

vector<int> int_list(5,10);//初始化为5个10
vector<int>::iterator iter=int_list.begin();// 迭代器

将每一个迭代引用的元素设置为0:

for (vector<int>::iterator i =int_list.begin();i!=int_list.end();i++)
    {
        *i=0;
    }

 

另外,所有的容器也还定义了另外一个迭代器,const_iterator,和iterator的区别是前者不可以修改容器元素的值。

vector<int>::const_iterator

不允许用const_iterator进行赋值操作。

任何改变vector长度的操作都会使已经存在的迭代器失效。比如调用push_back之后,就不能再信赖vector的迭代器的值了

数组:

数组的维数必须用大于等于1的整数常量表达式定义,这个常量表达式只能是整形字面常量、枚举常量、或者常量表达式初始化的整型const对象。非const变量以及要到运行阶段才知道其值的const变量都不能用于定义数组的维数。

int main()
{
    int getNumber();
    const int  max_files=30;
    const int otherNumber =getNumber();
    std::cout<<otherNumber<<std::endl;
    int staff_size=30;
    char input_buffer1[11];
    char input_buffer2[max_files];
    //char input_buffer3[staff_size];//错误,staff_size是局部变量
    //char input_buffer4[otherNumber];//错误,otherNumber是运行阶段才知道其值的const变量
    Console::WriteLine(L"Hello World");
    Console::ReadLine();
    return 0;

}


int getNumber()
{
    return 2;
}

字符数组比较特殊,如下

char ca1[] = {'C', '+', '+'}; // 缺少 null
char ca2[] = {'C', '+', '+', '\0'}; // 明确带上 null
char ca3[] = "C++"; // 自动添加上null

ca1 的维数是 3,而 ca2 和 ca3 的维数则是 4。

另外,不允许数组直接复制和赋值。需要注意的是,一些编译器允许将数组赋值作为编译器扩展。但是如果希望编写的程序能在不同的编译器上运行,则应该避免使用像数组赋值这类依赖于编译器的非标准功能。

与 vector 类型不同,数组不提供 push_back 或者其他的操作在数组中添加新元素,数组一经定义,就不允许再添加新元素。

在用下标访问元素时,vector 使用 vector::size_type 作为下标的类型,而数组下标的正确类型则是 size_t。

 

指针

与迭代器一样,指针提供对其所指对象的间接访问,只是指针结构更通用一些。与迭代器不同的是,指针用于指向单个对象,而迭代器只能用于访问容器内的元素。

在定义指针变量时,可用空格将符号 * 与其后的标识符分隔开来。如下三个写法都是正确的:

std::string* sp =&s;
std::string *sp =&s;
std::string*sp =&s;

而std::string* sp1,sp2;这句实质上定义了sp1这一个指针,而sp2则是普通的string变量。

很多运行时错误都源于使用了没有初始化的指针。

如果必须分开定义指针和其所指向的对象,则将指针初始化为 0。因为编译器可检测出 0 值的指针,程序可判断该指针并未指向一个对象。

说明:

指针中的*操作符的含义是对指针进行解引用获得该指针所指向的对象。解引用操作符返回左值,因此可为其结果赋值,等效于为该指针所指向的特定对象赋值

&操作符则是返回操作数对象在内存中的存储地址,不可赋值。

对指针进行初始化或者赋值只能使用一下四中类型的值:

  1. 0值常量表达式
  2. 类型匹配的对象的地址
  3. 另一对象末的下一地址
  4. 同类型的另一个有效指针

把int型变量赋值给指针是非法的,尽管此int型变量的值可能为0。

std::string s ="1234567";
std::string*sp =&s;
int zero =0;
int *ip=zero;//错误信息 error C2440: 'initializing' : cannot convert from 'int' to 'int *'
int *ip1 =sp;//错误信息 error C2440: 'initializing' : cannot convert from 'std::string *' to 'int *'    
int *ip2=&s;//错误信息 error C2440: 'initializing' : cannot convert from 'std::string *' to 'int *'

C++提供了一种特殊的指针 void *,他可以保存任何类型对象的地址

接上面的例子

void *ip1 =sp;//正确
void *ip2=&s;//正确

不过void *依然不能给赋值为int,即如下依然报错(注意保存信息是void):

void *ip=zero;//错误信息 error C2440: 'initializing' : cannot convert from 'int' to 'void *'

void* 指针只支持几种有限的操作:与另一个指针进行比较;向函数传递void* 指针或从函数返回 void* 指针;给另一个 void* 指针赋值。不能使用void*指针操纵它所指向的对象。

***注意如下,要cout字符串,必须#include <string>

std::string s1 ="1234567";
cout<<s1<<endl;//输出:1234567
sp =&s1;
std::cout<<sp<<endl;
*sp="7878";
std::cout<<*sp<<endl;//输出:7878

考虑以下两个程序段。第一个程序段将一个指针赋给另一指针:

int ival1 = 1024, ival2 = 2048;
int *pi = &ival1, *pi2 = &ival2;
pi = pi2; // pi现在指向ival2

 

赋值结束后,pi 所指向的 ival 对象值保持不变,赋值操作修改了 pi 指针的值,使其指向另一个不同的对象。现在考虑另一段相似的程序,使用两个引用赋值:

int &ri = ival1, &ri2 = ival2;
ri = ri2; // 将ival2的值赋值给ival1

这个赋值操作修改了 ri 引用的值 ival 对象,而并非引用本身。赋值后,这两个引用还是分别指向原来关联的对象(&ri和&ri2这两个地址的值依旧是原来的地址的值),此时这两个对象的值相等。

可以通过四个输出看出效果

int ival1=1204,ival2=2048;
int &r1=ival1,&r2=ival2;
std::cout<<r1<<","<<r2<<endl;
std::cout<<&r1<<","<<&r2<<endl;
r1=r2;
std::cout<<r1<<","<<r2<<endl;
std::cout<<&r1<<","<<&r2<<endl;//地址和上面的相同语句输出显示完全一样
    
Console::WriteLine(L"Hello World");
Console::ReadLine();

C++中,指针和数组密切相关,特别是在表达式中使用数组名时,这个名字会自动转换为指向数组第一个元素的指针。如下:

int ia[] ={0,2,4,6,8};
int *ip=ia;
int *ip1=&ia[0];
cout<<ip<<endl;//
cout<<ip1<<endl;
cout<<&ia[0]<<endl;
cout<<&ia[1]<<endl;
cout<<&ia[2]<<endl;
cout<<&ia[3]<<endl;
cout<<&ia[4]<<endl;

 

其中ip、ip1都等于&a[0],也就是数组的第一个元素的地址。

通常,在指针上加上(或者减去)一个整型数值n后,等效于获得一个新指针,这个新指针指向指针原来指向的元素之后(或者之前)的第n个元素。上面的指针例子用的是int*,可能效果不明显,换成string*来测试一下。

string ia[] ={"0","2","4","6","8"};
string *ip=ia;
string *ip1=&ia[0];
cout<<"ip:"<<ip<<endl;//
cout<<"ip1:"<<ip1<<endl;
cout<<"&ia[0]:"<<&ia[0]<<endl;
cout<<"&ia[1]:"<<&ia[1]<<endl;
cout<<"&ia[2]:"<<&ia[2]<<endl;
cout<<"&ia[3]:"<<&ia[3]<<endl;
cout<<"&ia[4]:"<<&ia[4]<<endl;
ip1=ip+1;
cout<<"ip1+1之后的值:"<<ip1<<endl;
Console::WriteLine(L"Hello World");
Console::ReadLine();

输出如图:

这里要注意这个n的值,看是否会超出数组的界。

两个指针做减法得到的值,可以判断这两个指针之间相差的元素的个数,减法的到的值得数据类型是ptrdiff_t(书写不必std::前缀)

    string ia[] ={"0","2","4","6","8"};
    string *ip=ia;
    string *ip1=&ia[0];
    string *ip3=&ia[3];
    ip1=ip+1;
    ptrdiff_t odd = ip3-ip1; //代码中没有using std::ptrdiff_t
    std::ptrdiff_t odd1 = ip3-ip1; //加上std::也没问题
    int odd2= ip3-ip1; //可以直接使用int
    cout<<odd<<endl;//输出2
    cout<<odd1<<endl;//输出2
    cout<<odd2<<endl;//输出2

 


 

【备注】关于ptrdiff_t的定义:

ptrdiff_t是C/C++标准库中定义的一个与机器相关的数据类型。ptrdiff_t类型变量通常用来保存两个指针减法操作的结果。ptrdiff_t定义在stddef.h(cstddef)这个文件内。ptrdiff_t通常被定义为long int类型。ptrdiff_t定义在C99标准中。

标准库类型(library type)ptrdiff_t 与 size_t 类型一样,ptrdiff_t 也是一种与机器相关的类型,在 cstddef 头文件中定义。size_t 是unsigned 类型,而 ptrdiff_t 则是 signed 整型。

这两种类型的差别体现了它们各自的用途:size_t 类型用于指明数组长度,它必须是一个正数;ptrdiff_t 类型则应保证足以存放同一数组中两个指针之间的差距,它有可能是负数。


 

解引用的操作非常方便:指针加上一个整型数,结果还是指针,因此可以按照如下操作来处理数据:

string newstr=*(ip+3);//newstr的值就是ia这个字符串数组中的索引为3(第四个)的元素:“6”

 

我们已经看到,在表达式中使用数组名时,实际上使用的是指向数组的第一个元素的指针。使用下标访问数组的时候,实际上是使用下标访问指针,在上面的string例子中:

string ia[] ={"0","2","4","6","8"};
string *ip=ia;

 

定义了*ip之后,ip的值是数组ia所在的地址的初始位置,而带有下标的ip[0]、ip[1]、ip[2]等等则分别是ia数组的索引分别0、1、2等的字符串"0"、"2"、"4"等。

ia元素个数是5,如下代码,ipend指向数组的最后一个元素,而ipend1则指向数组末端位置之后的位置了。我们可以输出ipend和ipend1这两个地址,但是只能输出*ipend(输出是“8”),而如果输出*ipend1的话,则是未知的乱码了。

string *ipend= ip+4;
string *ipend1= ip+5;

指针和const限定符

如果指针指向const对象,则不允许用指针来改变其所指的const值,因此C++语言强制要求指向const对象的指针也必须具有const特性,比如:

const double *cptr;

这里const限定的是cptr所指向的对象类型,而不是cptr本身,即ctpr本身并不是const。

在定义的时候不需要对它进行初始化,如果需要的话,允许给cptr重新赋值,使其指向另一个const对象。但是不能通过cptr修改其所指向的对象的值:

const int c=100;
const int d=100;
const int *cptr=&c;//初始化
cptr=&d;//正确,可以重新赋值,指向另一个const
*cptr=100;//错误,报错信息 error C3892: 'cptr' : you cannot assign to a variable that is const

 

上面这段代码,在执行了cptr=&d之后,下面的*cptr=100其实本质上就等同于d=100这个赋值操作,而d是const,自然这样写就是错误的。

对于void类型的指针,是一样的:

const void *vcptr=&c;
void *vptr=&c;//报错信息 error C2440: 'initializing' : cannot convert from 'const int *' to 'void *'

另一方面,我们可以将非常量的地址赋值给const对象的指针,比如这样写:

int cint =100;//非常量整型
cout<<cint<<endl;
const int *newptr=&cint;//赋值语句没问题
*newptr=101;//报错信息 error C3892: 'newptr' : you cannot assign to a variable that is const

如上可见,不能通过这个指向const的指针newptr给那个非const的整型变量cint进行赋值操作的。言外之意,指向const的指针一旦定义,就不能通过这个指向const的指针来修改它所指向的对象的值,即使这个指针指向的是非const对象也是如此。

记住两个概念:

  1. 由于没有方法分辨指向const的指针所指向的对象是否是const,系统会把它所指的所有对象都是为const。这也就是即使指向const的指针指向的对象不是const,也不能通过这个指针来给变量赋值的原因。
  2. 不能保证指向const的指针所指向的对象的值一定不可修改。因为如果这个对象不是const,是可以通过其他途径修改的,比如对象直接赋值或者通过指向这个对象的非指向const的指针。

指向const的指针应用于函数的形参,将形参定义为指向const的指针,以此确保参数的实际对象在函数中不会因为形参而被修改。

除了指向const的指针,还有另一个概念:const指针。

    int someNum=0;
    int * const sptr=&someNum;//const指针
    //sptr=&cint;//报错信息 error C3892: 'sptr' : you cannot assign to a variable that is const
    //sptr=0;//报错信息 error C2440: '=' : cannot convert from 'int' to 'int *const '
    *sptr=12;//正确,const指针所指向的对象的值可以修改,这句之后会导致下面
    cout<<someNum<<endl; //输出12

 

注意,这个const指针的概念就是:这个指针本身就是一个常量性质的对象(看C3892报错信息),因此给const指针做赋值操作是不对的,就像上面的报错信息所示:不能将整型指针赋值给。

总结一下指向const的指针和const指针:

const int *p :指向const的指针,指向可以修改,但是不能通过这个指针修改指向对象的值,也可以写成int const *p,声明的时候不是必须初始化。

int * const p :const指针,指针不可以修改,但是通过*p=xxx可以修改指向对象的值

两者结合在一起就是指向const对象的const指针:

const int *const lockptr=&c;
//lockptr=&c;//报错信息 error C3892: 'sptr' : you cannot assign to a variable that is const
//*lockptr=12;//报错信息 error C3892: 'sptr' : you cannot assign to a variable that is const

 

这里lockptr和*lockptr都不能做赋值操作,原因是lockptr是const常量,而*lockptr被系统当作常量处理(即使*lockptr所指向的c是一个非const的变量)

关于指针和typedef,如下声明const pstring的时候,const修饰的是pstring的类型,这是一个指针,因此该声明语句应该是吧cstr定义为指向string类型对象的const指针。如下三种声明方式是等价的

typedef string *pstring;
string someword="12";
pstring const cstr0=&someword;//1,const限定符既可以放在类型(pstring)后,
const pstring cstr=&someword;//2,const限定符也可以放在类型(pstring)之前
string *const cstr1=&someword;//3

在如下五个初始化语句中

int i = -1;
const int ic = i;
const int *pic = &ic;
int *const cpi = &ic;
const int *const cpic = &ic;

第四个是错误的.

报错信息 error C2440: 'initializing' : cannot convert from 'const int *' to 'int *const '

ic是const int类型,那么&ic则是const int * 类型,是指向const的指针;而cpi则是 int *const 类型,是const指针。

C++通过(const)char*类型的指针来操纵C风格字符串。

字符串字面值的类型就是const char 类型的数组。

在使用处理C风格字符串的标准库函数的时候,牢记字符串必须以结束符null结束:

char ic[]={'a','c'};
cout<<strlen(ic)<<endl;

 

这个例子中ic是一个没有null结束符的字符数组,这样计算的结果是不可预料的。标准库函数strlen总是假定其参数字符串以null字符结束,当调用该标准库函数时,系统将会从实参ic指向的内存空间开始一直搜索结束符,直到恰好遇到null为止,strlen函数返回的是这一段内存空间中总共有多少给字符,因此这个返回结果是不可信的。请这样写:

char ic[]={'a','c', '0'};
cout<<strlen(ic)<<endl;//输出是2,正确

    char largeStr[16+18+2];
    char cp1[]="A string example";//长度16
    char cp2[]="A different string";//长度18
    strncpy(largeStr,cp1,17);//17是16加上1个null
    cout<<strlen(largeStr)<<endl;//16
    strncat(largeStr," ",2);//2是空格加上1个null
    cout<<strlen(largeStr)<<endl;//17
    strncat(largeStr,cp2,19); //19是18加上1个null
    cout<<strlen(largeStr)<<endl;//35

    Console::WriteLine(L"Hello World");
    Console::ReadLine();

 

(说明:如果在代码中用strlen来求取cp1的长度,返回值是16,不包含字符串结束符)

使用strncpy函数(备注:关于strncpy和strcpy这两个函数的区别和比较见http://wenku.baidu.com/view/bd3f38f6ba0d4a7302763ad4.html)的时候,其中的参数17是cp1的长度16+1,这个多出来的1是必须的,因为这样才可以保证largeStr能够正确的结束。

而调用strncat的时候,要求赋值两个字符:一个空格和结束该字符串字面值的null。调用结束之后,字符串largeStr长度从16增加到17.

第二次调用strncat的时候,串接cp2,要求复制cp2中的所有字符,包括结尾的null。调用结束以后,总的字符串长度是16(cp1)+1(空格)+18(cp2)=35

整个过程中存储largeStr数组的总长度为始终保持为36(包含最后的null结束符)

所以,在使用C风格字符串处理的过程中要特别关注数组的容量和null结束符。不过,如果使用C++标准库类型string,则不存在上述问题,标准库负责处理所有的内存管理问题,我不必在担心每次修改字符串涉及到的大小问题。

 

相较于char的比较使用strcmp,string的比较使用成员函数compare:

    string str_a="a test";
    string str_b="a testing";
    int cmp_these =str_a.compare(str_b);
//---------------------------------------------------
    char *char_a="123";
    char *char_b="456";
    int cmp_char=strcmp(char_a,char_b);
//---------------------------------------------------

 

动态数组

数组类型的变量有三个限制:数组长度固定不变、在编译时必须知道其长度、数组只在定义它的块语句内存在。实际的应用中往往不能接受这样的限制。我们需要在运行时动态地分配数组,虽然数组长度是固定的,但动态分配的数组不必在编译时知道其长度,通常在运行时才确定数组长度。与数组变量不同的是,动态分配的数组将一直存在,知道程序显示释放它。

关于存储空间的分配和释放,在C语言中是使用malloc和free,在C++中则是使用new和delete。

比如:

    int *pia=new int[10];
    *(pia+5)=10;

 

new表达式分配了一个含有10个整型元素的数组,并返回指向该数组的第一个元素的指针,这个返回值初始化了指针pia。

动态分配数组的时候,如果数组元素具有类类型,将使用该类的默认构造函数实现初始化操作;如果数组元素是内置类型,则没有初始化:

如上图所示,蓝色箭头所示,psa数组在分配了内存空间之后,初始化为10个空字符串(*psa,*(psa+1),…,*(psa+9)的值都是””),而绿色箭头所示,int作为内置类型,pia在分配了内存空间之后,没有初始化。

我们也可以在分配长度的中括号后面加一对圆括号要求编译器对其进行初始化。比如:

    int *pia=new int[10]();

这样分配空间之后会给这10个元素初始化为0。

需要特别强调的是,对于动态数组,元素只能初始化为其元素类型的默认值,比如int只能初始化为0,string只能初始化为””,它无法像数组变量那样给每个元素提供不同的初始值。

 

C++虽然不允许定义长度为0的数组变量,但明确指出,调用new动态创建长度为0的数组是合法的:

    char char_c[1];//正确
    char char_d[0];//报错信息:unknown size
    char * char_p=new char[0];//编译通过

 

不过,该指针与new返回的其他指针不同,不能进行解引用操作,因为它毕竟没有指向任何元素。

动态空间的释放使用delete[]表达式释放指针所指向的数组空间:

delete [] pia;

注意:关键字delete和指针之间的孔方括号是必不可少的:它告诉编译器该指针指向的是自由存储区中的数组,而并非单个对象。如果一楼了方括号编译器并不会报错(理论上会导致运行时少释放了内存空间,从而产生内存泄露),这将会导致程序在运行时出错。

一个练习题:

编写程序由从标准输入设备读入的元素数据建立一个char 型 vector 对象,然后动态创建一个与该 vector 对象大小一致的数组,把 vector 对象的所有元素复制给新数组。

    vector<char>  iv;
    iv.push_back('2');
    iv.push_back('3');
    iv.push_back('1');
    char *char_iv=new char[iv.size()];
    char *i=char_iv;
    for (vector<int>::size_type st=0;  i!=char_iv+iv.size();++i,++st)
    {
        cout<<"pre:"<<iv[st]<<endl;
        *i=iv[st];
    }
    *(char_iv+iv.size())='\0';
    cout<<char_iv<<endl;
    for (char *tmp=char_iv;tmp!=char_iv+iv.size();++tmp)
    {
        cout<<tmp<<"---"<<*tmp<<endl;
    }

C++允许使用数组初始化vector对象,尽管这种初始化形式看起来有些陌生。使用数组初始化vector对象,必须指出用于初始化式的第一个元素以及数组最后一个元素的下一个位置的地址

    int a[4]={0,1,2,3};
    vector<int> vi (a,a+4);

 

传递给vi的两个指针标出了vector初值的范围,第二个指针只想被复制的最后一个元素之后的地址空间。当然,被标出的元素的范围可以是数组的子集,如下vi的元素个数是3:

    vector<int> vi (a+1,a+4);

将int型vector复制给int型数组,编码如下:

int *ia=new int[vi.size()];
    //写法1
    int k=0;
    for (int *i=ia;i<ia+vi.size();i++,k++)
    {
        *i=vi[k];
    }
    //写法2
    size_t ix=0;
    for (vector<int>::iterator iter =vi.begin();iter!=vi.end();++iter,++ix)
    {
        ia[ix]=*iter+1;
    }

 

关于多维数组的初始化,记住花括号有无是明显不同的:

    int a1[3][4]={{1},{2},{3}};//1,0,0,0,2,0,0,0,3,0,0,0
    int a2[3][4]={1,2,3};    //1,2,3,0,0,0,0,0,0,0,0,0

涉及到多维数组的指针概念,由于多维数组其实就是数组的数组,所以由多维数组转换而成的指针类型应是只想第一个内层数组的指针。

分析如下几个:

    int *p3=a;//a是一维整型数组,正确
    int (*p)[4]=a1;//正确
    int *p1 [4]=a1;//错误 error C2440: 'initializing' : cannot convert from 'int [3][4]' to 'int *[4]'
    int *p2 =a1;//错误 error C2440: 'initializing' : cannot convert from 'int [3][4]' to 'int *'

*p1的错误在于定义了一个指向int 的指针的数组;p2就更扯了,直接是定义的指向int的指针,这两种写法都不能正确初始化一个指向多维数组a1的指针,针对*p,正确的理解是:int[4]这样一个类型的指针,即p是指向一个元素是<4个int元素>的这样一个数组的指针。

我们还可以使用typedef简化写法:

    typedef int int_array[4];
    int_array *p100=a1;

int_array *p100代替了int (*p)[4]的写法。

位操作

关于位操作,一般而言,标准库提供的bitset操作更直接,更容易阅读和书写(set,reset)、易于使用。而且,bitset对象的大小不受unsigned数的为数限制,通常讲,bitset优于整型数据的低级直接位操作。

#include<bitset>
using std::bitset

    bitset<4> bitset_quiz1;//0--->0000
    bitset_quiz1.set(3);//8--->1000
    bitset_quiz1.set(2);//12--->1100
    bitset_quiz1.set(1);//14--->1110
    bitset_quiz1.reset(2);//10--->1010

关于i++和++i

前置操作需要做的工作更少,只需要加1后返回加1后的结果,而后置操作符则必须先保存操作数原来的值,以后返回未加去之前的值作为操作的结果。对于int型对象和指针,编译器可优化掉这项额外的工作,但是对于更多的复杂迭代性类型,这种额外工作可能会花费更大的代价。因此建议,只有在必要的时候才使用后置操作符。

 

关于解引用的优先级问题

SomeClass *p=&item;

(*p).method(xx);

解引用的优先级低于点操作符,所以上述用法中,需要注意必须用圆括号把解引用括起来。不能写成下面的形式:

*p.method(xx);

这样含义就完全变了,等价于*(p.method(xx));

针对这种情况,C++有另外一个操作符:箭头操作符(->).

(*p).method(xx);等价于p->method(xx);

 

sizeof操作符:

sizeof操作符的作用是返回一个对象或者类型名的长度,返回值的类型是size_t,长度的单位是字节。size_t表达式的结果是编译时常量,该操作符有三种语法形式:

sizeof (type name);

sizeof(expr);

sizeof expr;//没有括号

    int little_i,*ip;
    string little_str,*sp;
    cout<<sizeof(little_i)<<endl;//输出4
    cout<<sizeof(int)<<endl;//输出4
    cout<<sizeof(little_str)<<endl;//输出32
    cout<<sizeof(string)<<endl;//输出32
    cout<<sizeof(ip)<<endl;//输出4,ip的类型的size
    cout<<sizeof(*ip)<<endl;//输出4,ip所指向的对象的类型(int)的size
    cout<<sizeof(sp)<<endl;//输出4,sp类型的size
    cout<<sizeof(*sp)<<endl;//输出32,sp所指向的对象的类型(string)的size
    cout<<sizeof(&sp)<<endl;//输出4,sp的地址的size
    cout<<sizeof int<<endl;//报错,对于类型必须使用括号

 

将sizeof用于表达式的时候,并没有计算表达式的值,特别是在sizeof *p中,指针p可以持有一个无效地址,因为不需要对p进行解引用操作

关于sizeof有如下几个性质:

  1. char类型或者值为char类型的表达式做sizeof操作肯定等于1
  2. 对引用类型做sizeof操作将返回存放词引用类型对象所需的内存空间大小
  3. 对指针做sizeof操作将返回存放指针所需要的内存空间大小(sizeof(sp)),如果要获取该指针所指向对象的大小,则必须对指针进行引用(sizeof(*sp))。
  4. 对数组做sizeof操作符的值除以对其元素类型做sizeof操作的值,返回值就是数组元素的个数
int ia_new[3]={1,2,3};
    cout<<sizeof(ia_new)<<endl;//输出12
    cout<<sizeof(ia_new)/sizeof(*ia_new)<<endl;//输出3

动态创建对象的默认初始化,对于类类型的对象,用该类的默认构造函数初始化,而内置类型的对象则无初始化:

    int *pi1=new int;//无初始化
    string *ps1=new string;//初始化为空字符串

 

也可以对动态创建的对象做值初始化

    int *pi1=new int();//初始化为0
    string *ps1=new string();//初始化为空字符串

值初始化的()语法必须置于类型名后面,而不是变量后。

 

一旦删除了指针所指向的对象,立即将指针置为0,这样就非常清楚地表明指针不再指向任何对象。

 

操纵动态分配内存的常见三种错误:

  1. 删除指向动态分配的指针失败,因而无法将该块内存返还给自由存储区,造成内存泄漏。
  2. 读写已经删除的对象。如果删除指针所指向的对象之后,将指针职位0,则比较容易检测出这类错误。
  3. 对同一个内存空间使用两次delete表达式。

 

类型转换的指针转换:

在使用数组时,大多数情况下数组都会自动转换为指向第一个元素的指针:

    int ai[10];
    int *pi = ai;//*pi ==ai[0]

C++还提供了另外两种指针转换:

  1. 指向任意数据类型的指针都可转换为void*类型;
  2. 整型数值常量0可以转换为任意指针类型。

 

显示转换也成为强制类型转换(cast),包括以下强制类型转换操作符:static_cast,dynamic_cast,const_cast,reinterpret_cast。

(说明:在引入命名的强制类型转换操作符之前,显示强制转换用圆括号将类型括起来实现,效果和使用reinterpret_case符号相同,但是这种强制转换的可视性较差,难以跟踪错误的转换。为了加强类型转换的可视性,所以标准C++引入了命名的强制转换操作符。对比一下C#的类型转换)

强制类型转换的原因有:

  1. 要覆盖通常的标准转换
  2. 可能存在多种转换时,需要选择一种特定的类型转换

引用传递数组

通过引用传递数组,属性形参可以生命为数组的引用。如果形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身,这种情况下,数组大小成为形参和实参类型的一部分,编译器检查数组的实参大小和形参大小是否匹配:

void printValues(int (&arr)[10]){
    //nothing
}


int main(array<System::String ^> ^args)
{
    int i=0,j[2]={0,1};
    int k[10]={0,1,2,3,4,5,6,7,8,9};
    printValues(&i);//错误信息 error C2664: 'printValues' : cannot convert parameter 1 from 'int *' to 'int (&)[10]'
    printValues(j);//错误信息    error C2664: 'printValues' : cannot convert parameter 1 from 'int [2]' to 'int (&)[10]'
    printValues(k);

    Console::WriteLine(L"Hello World");
    return 0;
}

这个版本的printValues函数只严格接受含有10个int型数值的数组。

注意&arr两边的圆括号是不许的,因为下表操作符具有更高的优先级。

void printValues(int &arr[10]):这样会报错,因为参数变成了一个数组

 

函数返回引用:当函数返回引用类型的时候,并没有复制返回值,相反,返回的是对象本身。例如,下面这个函数返回两个string类型形参中较短的那个字符串的引用:

const string &shorterString(const string &s1,const string &s2)
{
    return s1.size()<s2.size()?s1:s2;
}

形参和返回类型都是指向const string对象的引用,调用函数和返回结果时,都没有复制这些string对象。

理解返回引用至关重要的是:千万不能返回局部变量的引用。当函数执行完毕时,将释放分配给局部对象的存储空间。此时,对局部对象的引用就会指向不确定的内存。比如下面这段代码:

const string &tmpString(const string &s)
{
    string tmp =s;
    return tmp;//wrong
}

这个函数会在运行时出错,因为它返回了局部对象的引用。当函数执行完毕的时候,字符串tmp占用的存储空间被释放,函数返回值指向了对于这个程序来说不再有效的内存空间。

 

返回引用的函数返回一个左值。因此这样的函数可用于任何要求使用左值的地方:

char &get_val(string &str,string::size_type ix)
{
    return str[ix];
}

int main(array<System::String ^> ^args)
{
    string s("a value");
    cout<<s<<endl;//输出a value
    get_val(s,0)='A';//将第一个字符是s[0]修改为A,★★
    cout<<s<<endl;//输出A value
    Console::ReadLine();
    return 0;
}

 

给函数返回值赋值这可能有点不合常理,但是由于函数返回的是一个引用,因此这样做是正确的,这个引用时被返回元素的同义词。

如果不希望引用返回值被修改,可以将返回值声明为const:

const char &get_val(string &str,string::size_type ix)
{…………

同样,也不可以返回指向局部对象的指针,原因同样是函数结束的时候,局部对象被释放掉,返回的指针就变成了指向不存在的对象的悬垂指针。

 

函数声明

正如变量必须先声明后使用一样,函数也必须在被调用之前先声明。与变量定义类似,函数的声明也可以和函数的定义分离:一个函数只能定义一次,但是可声明多次。函数声明由函数返回类型函数名形参列表组成。这三个元素被称为函数原型,描述了函数的接口(可以联想一下C#中做wcf服务函数和对应接口中的函数声明这块内容)。形参列表必须包括形参类型,但是并不必须有形参命名

默认实参

需要注意的原则是如果有一个形参具有默认实参,那么其后面所有的形参都必须有默认实参,比如:

string connectString(string s1,string s2,string s3="s3",string s4="s4")
{
    return s1+s2+s3+s4;    
}

关于形参、局部变量和静态局部变量的概念:

本质上说,形参、局部变量和静态局部变量都属于局部作用域中的变量。其中,局部变量有可以分成普通(非静态)局部变量和静态局部变量。区别如下:

1 形参的作用域是整个函数体,而普通局部变量和静态局部变量的作用域是从定义处到包含该变量定义的块的结束处。

2 形参由调用函数时所传递的实参初始化。而局部变量(包括静态局部变量)通常用初始化式来进行初始化,在程序执行流程第一次经过该对象的定义语句时进行初始化。静态局部变量的初始化工作在整个程序执行过程中只执行一次。

3 形参和普通局部变量均属于自动对象(当定义它的函数被调用时才存在的对象)。自动对象每次调用函数时创建和撤销(即每次调用函数时创建,函数结束时撤销)。而静态局部变量的生命期是跨越了函数的多次调用,静态局部变量在创建后是直到程序结束时才撤销,所以静态局部变量从概念上说,不属于自动对象。

 

内联函数

内联函数的用法是在函数定义前面加上inline:

inline string connectString(string s1,string s2,string s3="s3",string s4="s4")
{
    return s1+s2+s3+s4;    
}

内联函数的作用是避免函数的开销(在大多数机器上,调用函数都要做很多工作:调用前先保存寄存器,并在返回时恢复;复制实参;程序还必须专项一个新位置执行)。

内联有两点需要注意:

1 对于编译器来说,内联只是一个建议,编译器可以选择忽略这种方式

2 内联机制适用于优化小的、只有几行且经常被调用的函数。大多数编译器都不支持递归函数的内联;上千行的函数也不太可能在调用时内联展开。

 

类的成员函数

1 类的所有成员都必须在类定义的花括号里面声明

2 成员函数的定义可以在类的定义里面,也可以在类的定义外面。在类的定义外面定义成员函数必须指明它们是类的成员。

class ClassName
{
public: double method() const;
protected:
private:
};

double ClassName::method() const
{
    return 0;
};

 

3 编译器隐式地将类内定义的成员函数当作内联函数。

4 this指针:每个成员函数(不包括静态成员函数)都有一个额外的、隐含的形参this。这个this用于指示当前对象,在调用成员函数的时候,形参this初始化为调用函数的对象的地址。this指针是隐式定义的,在函数的形参表中不可以包含this指针;在函数体中可以显示地使用this指针(当然也不是必要的)。

5 常量成员函数:使用const限定的成员函数,只能读取调用它们的对象,而不能修改对象的数据成员。const对象、指向const对象的指针或引用只能用于调用其const成员函数,不能用它们来调用非const成员(即实例成员)。

6 在任何函数定义中,返回类型和形参表必须与函数声明(如果有的话)一致。对于成员函数,函数声明必须与其定义一致,如果函数被声明为const成员函数,那么函数定义时形参表后面也必须有const。

构造函数

构造函数属于特殊的成员函数。与其他成员函数不同,构造函数和类同名,而且没有返回类型。与普通成员函数相同的是,构造函数有也可以有形参表(可以为空)和函数体,一个类可以有多个构造函数,每个构造函数必须有与其他构造函数不同数目或者类型的形参。构造函数一般放在public限定下,放在private下也不会报错,但是没有意义。

如下类定义中,ClassName的带有一个参数的构造函数冒号后面的filed(“haha”)属于构造函数的初始化列表。这个初始化列表为类的一个或者多个数据成员指定初值,多个成员的初始化用逗号分隔。当然这个初始值的指定也可以放在构造函数的函数体内。

class ClassName
{
public: double method() const;
        string field;
        string field2;
        ClassName()
        {
            field="field";
        }
        ClassName(string a):field("haha"), field2("haha2")
        {
            
        }
protected:
private:
    ClassName(string a,string b )
    {

    }
};

 

 

如果类没有显示定义任何构造函数,编译器将自动给这个类生成一个构造函数,由编译器创建的这个构造函数通常称为默认构造函数。对于具有类类型的成员,默认构造函数会根据成员所属类自身的默认构造函数实现初始化;内置类型成员的初值则依赖于对象如何定义。如果对象在全局作用域中定义或定义为静态局部对象,则这些成员被初始化为0;如果对象在局部作用域中定义,则这些成员不初始化。

函数重载

出现在相同作用于中的两个函数,如果具有相同的名字而形参表不同,则称为重载函数

(备注:当前在C#,php等其他高级语言中,很多讲解资料将子类覆写了基类的同名函数也叫成重载,我感觉不对,应该是个人说法上的习惯问题。override和overload)

函数重载和重复声明的区别:如果两个函数声明的函数名、返回类型和形参表完全匹配,则将第二个函数声明视为第一个的重复声明。如果两个函数的形参表完全相同,但返回类型不同,则第二个声明是错误的。

函数不能仅仅基于不同的返回类型而实现重载。在这个概念中,函数声明中的形参名又或者没有、形参名是什么,这些都不会影响到形参表的本质(还有还注意的是如果使用了typedef,那么使用原类型名和用typedef定义的类型名也是一样的,不会影响形参表)。也就是说,以下三个函数声明完全是一样的:

std::ostream& output(std::ostream&) const;
std::ostream& output(std::ostream& out) const;
std::ostream& output(std::ostream& out1) const;

 

另外,这三个函数声明都可以对应如下的函数定义:

std::ostream& NewSales_item::output(std::ostream& out) const
{
……..
    return out;
}

重载和作用域

关于重载和作用域的概念,我们用下面这个例子来说明:

void print(const string &);
void print(double); //两个print的声明
void fooBar(int ival)
{
void print(int); // 这个局部声明会屏蔽掉上面两个print声明
print("Value: "); // 错误: print(const string &) 被屏蔽掉了,另外[value:]无法转型为int
print(ival); // 正确: print(int) 声明可以使用
print(3.14); // 正确:3.14转型为int的3,调用print(int); 另外,print(double) 被屏蔽掉了
}

 

当然,局部声明函数是一种不明智的选择,函数的声明应该放在头文件中。再看下面这个重载方式:

void print(const string &);
void print(double); //重载1
void print(int); // 重载2
void fooBar2(int ival)
{
print("Value: "); // 正确: 调用 print(const string &)
print(ival); //正确: 调用print(int)
print(3.14); //正确: 调用 print (double)
}

 

关于这块内容,需要程序员合理设计形参集合,避免在编译器进行重载函数的参数匹配、调用哪个重载函数的时候出现二义性。

 

关于重载和const形参,仅当形参是引用或者指针的时候,形参是否为const才有影响。换句话说,当形参以副本传递时,不能基于形参是否为const来实现重载。

对于引用形参,如果形参是普通的引用,则不能将const对象传递给这个参数。如果传递了const对象,则只有带有const引用形参的版本才是该调用的可行函数。非const对象既可以用于初始化const引用,也可以用于初始化非const引用。但是,将const引用初始化为非const对象,需要通过转换来实现,而非const形参的初始化则是精确匹配。

对于指针形参,编译器可以判断:如果实参是const对象,则调用带有const*类型形参的函数;否则,如果实参不是const对象,则将调用带有普通指针形参的函数。

注意不能基于指针本身是否为const来实现函数的重载。

f(int *);

f(int *const); // 重复声明

此时,const用于修改指针本身,而不是修饰指针所指向的类型。

指向函数的指针

函数指针是指指向函数而不是指向对象的指针。像其他指针一样,函数指针也指向某个特定的类型,函数类型尤其返回类型以及形参表确定,与函数名无关。

bool (*pf)(const string &, const string &);

这个语句将pf声明为指向函数的指针,它所指向的函数带有两个const string&类型的形参和bool类型的返回值。注意*pf两侧的圆括号是必须的

函数指针类型相当的冗长,我们可以使用typedef来给指针类型的使用进行简化:

typedef bool (*cmpFcn)(const string &, const string &);

这样,就用cmpFcn来代表了这种指针类型。

在引用函数名但又没有调用该函数的时候,函数名将被自动解释成指向函数的指针。假设有如下函数:

bool lengthCompare(const string &, const string &);

除了用作函数调用的做操作数以外,对lengthCompare的任何使用都被解释为如下类型的指针:

bool (*)(const string &, const string &);

因此可以使用函数名对函数指针做初始化或者赋值:

cmpFcn pf1 = 0; // 正确: 未绑定的函数指针
cmpFcn pf2 = lengthCompare; // 正确: 指针类型和函数的类型匹配
pf1 = lengthCompare; //正确: 指针类型和函数的类型匹配
pf2 = pf1; // 正确: 两个指针类型匹配

此时,直接引用函数名等效于在函数名上应用取地址操作符:

cmpFcn pf1 = lengthCompare;
cmpFcn pf2 = &lengthCompare;

 

注意,不同函数类型的指针之间不存在转换。

那么,函数指针具体用在什么地方呢?使用指向函数的指针可以用于调用它所指向的函数,可以不需要使用解引用操作符,直接通过指针调用函数:

cmpFcn pf = lengthCompare;
lengthCompare("hi", "bye"); // 直接调用
pf("hi", "bye"); // 等价调用: pf1的间接逆向引用
(*pf)("hi", "bye"); // 等价调用: pf1的间接逆向引用

注意,只有函数指针初始化了,或者赋值为指向某个函数,才能安全地用来调用函数。

 

函数的形参可以是指向函数的指针

这种形参有两种形式编写:

void useBigger(const string &, const string &,bool(const string &, const string &));

void useBigger(const string &, const string &,bool (*)(const string &, const string &));

注意第三个参数,第一种方式中表明是函数类型,会自动作为指针处理;第二种则通过(*)明确指示其为函数指针。

 

函数可以返回指向函数的指针

不过这种返回类型书写不易,而且也较难理解:

int (*ff(int))(int *,int);

阅读函数指针声明的最佳方法是从声明的名字开始从里到外来理解。首先看ff(int),将ff声明为一个函数,带有一个int型的形参,这个函数的返回是int(*)(int*,int),这是一个指向函数的指针,所指向的函数返回int型,并带有两个分别是int*和int类型的形参。

 

可以使用函数指针指向重载的函数

但是指针的类型必须与重载函数的一个版本精确匹配,否则会编译错误。

 

回顾C++ Primer

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!