函数
1.函数调用知识
函数处理过程
- 函数定义 + 函数原型+调用函数 = 函数处理过程
void fun(int a,int b)//函数原型
int main(){
int a,b;
fun(a,b);//函数调用
}
void fun(int a,int b){//函数定义
...
}
函数返回值原理:
函数执行到return后会将返回值复制到指定的内存单元或寄存器(临时变量)中,主调用程序查看此单元,所以,返回的是一个内存单元,或者说一个值(地址)。
因而,返回的不能是数组,原因有:数组名本身就是一个地址,无法通过一个数组首地址了解数组的整体存储情
况,而且在函数被调用结束后会自动释放函数里申请的局部内存。
*
`int fun(int a[]){<br />
int *b=new int;//动态申请的空间不会被销毁`,为什么?因为这个流程是:1.在堆内申请了一块int型区域,2.分配一个指针变量b指向该区域 3.在该函数块结束后指针变量销毁,返回的b的内容到临时变量中,而new的空间不会销毁———详见第二节
...
return b;
}
int *fun(int *a,...){
...//因为是地址传递,可修改a指向数组的值
return a;
}
指针参数传递也是值传递,只不过传递的是地址值,同样会再开辟空间存放副本,形参的改变不影响实参;
- 引用传递:此时在函数栈中开辟空间存放的不是实参的副本,是实参的地址,相当于形参是间接寻址,对形参的操作会影响实参。
值传递: void exchange(int a,int b){ int t=a; a=b; b=a; //交换失败 } 地址传递1: void exchange1(int a,int b){ void exchage2(int a,int b){ int t=a; int t=a; a=b; a=b; *b=t; //交换成功 b=t;//交换失败 } 引用传递: void exchange(int &a,int &b){ int t=a; a=b; b=t; }//交换成功!!
2.函数与数组
- 数组可以作为参数传递,例如fun(int a,..)==fun(int arr[],..)。但是当且仅当这里二者相同,`int a
表示a是一个int型的指针,而
int arr[]`表示arr指向了int数组的第一个字节。而且sizeof(a)与sizeof(arr)也不一样。
同时,数组作为参数传递时只提供了一个数组首地址,需要在传入数组大小,例如fun(int *a,int n).
sizeof(arr);//大小是数组整个字节数目
sizeof(a);//只是一个指针变量的长度,各有不同
指针与const
int a=1;
const int *pt=&a;
//const 修饰 pt,表示pt指向的值看作常量,不可修改,但pt可变,而且可通过a修改。
`int const pt=&a;`//const修饰的pt指针看作常指针,不可变,但*pt可变
c++禁止const的地址赋给非const指针 int *pd=&a;
const int *pt=pd;
//!!不合法
所以,函数的指针参数时尽量用constant修饰,避免修改实参数据。
3.函数与指针(不常用)
- 函数与c-风格字符串:char数组,引号括起的字符串常量,char *s的字符串地址的char指针
他们都是以’\0’为结束标志,其ASCII码为0.
char * func(char c,int n){
char *ptr = new char[n + 1];
ptr[n] =' \0';
while (n-- > 0){
ptr[n] = c;
return ptr;
}// 这里的函数结束后,ptr使用的变量内存将被销毁,而不是字符串。由于返回了ptr的内容,main()仍可以访问这里的字符串。
//同时,记住要delete [] ptr;
- 函数指针意味着一个函数的地址作为另一个函数的参数。
获取函数地址:函数名即可,但要区分传递的参数是函数地址还是函数的返回值。
process(think); //think是函数的地址作为参数
process(think()); //将think()的返回值作为参数
声明函数指针:
double (pf)(int); //pf是指向一个函数,这个函数的返回值是double,而且这个函数有一个int型参数
double pf(int); //pf()是一个函数,它返回的是double*型的指针
4.auto(c++11)
- 自动类型转换
5.内联函数
一般函数的调用指令是首先将该指令地址记下来,然后跳转到函数的起点执行被调用函数,执行完毕后跳回到原来的地方。可能需要来回跳跃到某处执行该区域的代码,而内联函数是将编译代码与其他程序代码“合”起来了,编译器把相应的函数代码替换了调用函数的语句。使得运行速度块,但占用更多内存。
内联函数原型:inline int func(...);
内联函数定义:inline int func( ... ) { ... ; return ... ; }
6.默认参数
是指当函数调用时,省略了实参时自动使用的一个值,并且在函数原型中设置即可。如:double * func(int a=1,int b=2);
//函数原型void main(){ func(2); }
//函数调用,func(int a,int b){ cout<<a<<" "<<b<<endl; }
//函数定义
此时输出a=3,b=3;
注意事项:
- 带参数列表的必须从右向左添加默认值
int chico(int n,int m=1,int k);
//错误
- 实参必须按照从左往右顺序依次赋值给形参,不能跳过某个参数。
7.函数重载
指函数名相同,返回类型相同,但参数列表不同的重载。仅当执行相同的任务而且使用不同类型的数据时,重载才会有用处。
8.函数模板
引用
1.引用变量的原理
&在等于号=左边是引用符,右边是取址符。引用是变量的别名,不是指针,因为引用的地址与原变量的地址相同,引用的内容与原变量的内容相同。相当于一个变量,两种名称(叫法)。
int a=10;
int &a1=a;
cout<<"a="<<a<<" "<<"&a="<<&a<<endl;
cout<<"a1="<<a1<<" "<<"&a1"<<&a1<<endl;
注意事项:
- 引用变量的创建声明必须同时初始化,一旦与原变量关联起来,就一直效忠于它。
正确:int & a1 = a;
错误:int rat;
int & r ;
r = rat;
而且如果将别的变量值试图重新赋给引用变量,虽然该变量的值发送变化,但是其与之关联的(地址)一直没变。
int a=10; int &a1=a; cout<<"a="<<a<<endl<<"&a="<<&a<<endl; cout<<"a1="<<a1<<endl<<"&a1"<<&a1<<endl; int b =5; a1=a; cout<<"b="<<b<<endl<<"&b="<<&b<<endl; cout<<"a1="<<a1<<endl<<"&a1="<<&a1<<endl;
2.引用作为函数参数的特别之处
对于引用的参数传递中形参是实参变量的别名,所以,实参必须是变量。
void func(int &a);//函数原型
func(a1+10);//a1+10不是变量,报错
在早期的c++中会有临时创建一个无名变量来初始化表达式的值,然后a成为该无名变量的引用别名。现在,仅 当const引用时会有上述流程。
在const引用时,当实参与形参的引用不匹配时,会生产临时变量。- 实参类型正确,但不是左值。
- 实参类型不正确,可以转化。
左值:可以引用的变量名=常规变量+const变量,如变量,结构成员,数组元素,引用的指针。 右值:字面常量(用引号括起的字符串除外,他们由地址表示的)+ 多项表达式。
所以,一般设置引用参数是为了对实参的内容改变,否则最好使用const的引用来确保原始数据不会变化,采用临时变量进行局部存储。好处:但是,不用返回指向局部变量或临时变量对象的引用,因为函数执行完毕后,局部变量和临时对象消失,引用会指向不存在的数据。(虽然编译器可以通过或正确,但是要知道这种做法是不稳定的。)
- const避免无意对原始数据修改的错误
- constant使函数可以处理非constant和constant实参;
- const引用可以让函数生产临时变量。
double func(const int &a);
另外,c++11加入了右值引用,可以引用右值。int a = 10;
int & b = a + 11;
3.总结引用
使用引用参数的原因:
- 想要修改调用函数中的数据对象(实参)。
通过传递引用而不是值传递整个数据对象,提高程序运行速度。
数据对象是较大结构,使用const指针或constant引用。
-
使用值传递而不用引用传递的:
数据对象很小,如内置数据类型。
数据对象是数组,则使用指针传递,这是唯一选择,且设为const。
类和对象
面对对象的编程00P的特性:抽象,封装数据隐藏,多态,继承,代码可重用性。
1.类的成员和方法
类的设计是为了将公有接口和私有数据分开。首先,一般将类的数据表示,函数原型放在一个头文件.h中;将定义函数的源代码放在方法文件cpp中,如此就把接口和实现细节分开,测试程序只通过接口进行通信。如图所示。
注意事项:在类中,如果没有对成员/方法的访问权限控制显示的修饰,则默认是private。
- 结构体与类的区别在于结构体只是单纯的封装数据,不添加方法,是对几种不同数据类型的绑定,默认访问public。
由于把类的成员函数原型和定义实现分开了,在实现cpp文件中,成员函数的函数头使用作用域解析运算符::来指出函数所属的类。
vodi Stock:: uodate(){…}
类的内联方法:将内联定义放在定义类的头文件中。
2.类的构造和析构函数
类的对象在创建是要初始化,即调用构造函数。
- 显示调用构造函数:
Stock food = Stock("w",250,1.25);
- 隐式调用构造函数:
Stock food("w",250,1.25);
类实现cpp中:Stock::Stock(string s,int x,double y){….};//有参数的构造函数
- 调用默认构造函数:
Stock food;
//隐式调用Stock food();
类实现cpp中,定义默认构造函数:
Stock::Stock(){};//默认构造函数,隐式调用时 Stock :: Stock(string &co =”Error”, int n = 0,double p = 0.0);
注意:默认构造函数的调用必须是类中没有书写构造函数的情况才会被调用,当类有了构造函数后,该种调用就不合法,所以必须再为类提供默认构造函数。
new构造
Stock *food =new Stock("w",250,1.25);//返回对象指针
析构函数:
完成内存的清理工作,在主程序中一般不用显示的调用,对象如果在生存期间用 new 运算符动态分配了内存,则在各处写 delete 语句以确保程序的每条执行路径都能释放这片内存是比较麻烦的事情。有了析构函数,只要在析构函数中调用 delete 语句,就能确保对象运行中用 new 运算符分配的空间在对象消亡时被释放。有时函数中的临时对象的创建和消亡也会调用析构函数。链接
3.this指针
this指针指向用来调用成员函数的对象,如stock1.topval(stock2)则this是stock1的地址。*this是整个对象的引用。
- 有时,int n = 10; int a[n]={0}; //行不通,n是局部变量
但是,const int n =10; int a[n]={0};//可以
类的动态内存分配
首先,分成类声明头文件,类方法实现cpp文件,测试使用类cpp文件来体现opp。
- 如果在构造函数中使用new来分配内存,必须在相应的析构函数中使用delete将其释放,而且最好使用new []与delete[]。删除对象可以释放对象本身占用的内存,但不能自动释放属于对象成员的指针指向的内存。
- 类方法实现cpp文件用于对数据的初始化,不能放在类声明头文件中,而且类方法中要使用作用域预算符::来指出该方法或该静态成员所属的类。
- 复制构造函数
将一个类的对象复制到新创建的对象中,经常用于初始化过程。
Class_name(const Class_name &…);
- 何时调用复制构造函数:当创建某一对象副本或临时对象是,编译器会调用复制构造函数,例如按值传递对象和返回对象的引用皆会调用。
A类,a是已经初始化的对象 A a1(a); A a1=a; A a1 = A(a); A *a1=new A(a);
而默认复制构造函数只逐个复制对象内非静态的成员,是一种浅复制,当一些成员是new初始化,指针时就不会复制数据本身。例如字符串表示的时候只复制了字符串地址和长度。下面展示一种字符串的显示深度复制构造函数:
StringBad::StringBad(const StringBad & st){ len = st.len; str = new char[len+1]; strcpy(str,st.str); }
- 赋值运算符
Class_name & Class_name::operator=(const CLass_name & …);
何时调用复制运算符?在将已有对象赋给(!=初始化)另一个对象时调用。
StringBad head(“ss”); StirngBad k; k=head;//调用赋值运算符 String knot = head;//这时 是初始化,不一定使用赋值运算符。
目标对象可能以前分配的数据,需要delete[]释放。
- 避免将对象赋给自身,否则在对象重新赋值之前会删除自身对象内容。
- 谈谈在构造函数中new的注意事项
- new和delete必须兼容,new对应delete,new[]对应delete[]。
- 如果想用对象去初始化另一个对象时,需要定义一个辅助构造函数进行深复制。
- 如果想用对象去赋值给另一个对象,需要定义一个重载的赋值运算符。
- new还有一种定位运算符的作用,在已经开辟的堆内存中在细分new出一个空间
char buffer=new char[521]; double p1 = new (buffer)double[5]; // p1与buffer的地址相同
名称空间
每一个变量,函数,结构体都有自己的作用域空间,当名称重复时就要控制名称的作用域以防止冲突。
c++关于局部变量和全局变量的规则定义了一种名称空间层次。每个声明区域都可以声明名称,这些名称独立于在其他声明区域中声明的名称。在一个函数(名称空间)中声明的局部变量不会与在另一个函数(名称空间)声明的局部变量冲突。
例如:用namespace来声明名称空间。
namespace Jack{ double pail; int pal; } namespace Jill{ double pail; int pal; } //当想要指定某个特定名称空间的变量时用作用域解析符:: Jack:: pail =1.1; Jill::pail = 1.0;
using声明将特定空间的局部变量引入它所在的作用域,然后可以直接使用变量名称来代替名称空间的同名变量。
namespace Jack{ double pail; int fetch; } int fetch; int main(){ using Jack::pail;//把Jack空间的变量引入 double pail;//ERROR,重复定义了一个pail局部变量 }
using编译指令是using namespace + 名称空间名组成,使该名称空间中所有名称可用。
如 using namespace std;//把iostram等头文件放到std空间。 std命名空间是C++中标准库类型对象的命名空间,不加的话需要std::cout….
- 名称空间和声明区域定义了相同的名称,如果再用using声明将名称空间的名称导入,则两个名称会发生冲突,如果使用using 编译指令导入,则局部的名称会覆盖名称空间的版本。
namespace Jack{ double pail; int fetch; } int fetch; int main(){ using namespace Jack; int fetch;//可以,但会隐藏Jack::fetch cin>>fetch;//输入一个局部变量 cin>>::fecth;//输入一个全局变量 cin>>Jack::fecth;//输入一个Jack的fetch }