1.内存四区
1.1 内存分区模型, C++程序执行时,将内存大方向分为四个区域:
代码区:存放函数体的二进制代码,由操作系统进行管理。
全局区:存放全局变量和静态变量以及常量。
(C++中常量分很多种,比如整型常量、字符串常量等,常量最常用的就是给变量赋值。例如:int a = 10; 其中a是变量,10是常量。全局变量:全局变量是定义在所有函数之外的变量,包括主函数。全局变量可的生命周期是定义处开始到整个程序结束,全局变量可被在其定义处以后的函数调用。如果局部变量与全局变量重名,那么在局部变量的作用域内全局变量无效。全局变量少用,除了增加内存开销外,还增加了程序理解的难度。C++中的静态成员:我们需要类中的成员与类本身相关而不与类的对象相关,这就用到静态。例如:一个银行账号类,它有户主名等成员还有一个基准利率数据成员,而我们希望基准利率只与银行账号类有关而不与张三的账号等类的对象有关,那么就可以将其设置为静态成员,其中最主要的就是我们希望在利率浮动时,所有对象都能用到新值。静态变量的静态指在整个程序生命周期的地址静止不变。如果一个静态变量被访问后值改变,它就会保持新的值。)
栈区:由编译器自动分配释放,存放函数的参数值(形参),局部变量等。
堆区:有程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
(例如使用malloc、new等函数)
内存四区的意义:不同区域存放的数据赋予不同的生命周期,给我们更加灵活的编程。
1.2 在程序编译后,生成了".exe"文件(可执行文件),在运行可执行文件之前,有两个区域:
一个是代码区,代码区存放CPU执行的机械命令;代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区是只读的,目的是防止被意外更改。
一个是全局区,全局变量和静态变量(包括局部静态变量)存放在此。全局区还包括了常量区,字符串常量和其他常量(const修饰的全局变量,局部常量即const修饰的局部变量不在全局区中)存放于常量区。该区域存放的数据一旦经初始化后就不能被修改,在程序结束后由操作系统释放。
1.3 在程序运行后,系统又划分的两个区域
一个是栈区,注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放。
int *func()
{
int a = 10; //定义局部变量
return &a; //返回局部变量地址
}
int main()
{
int *p = func();
cout << *p << endl; //返回10,正确,因为编译器做了保留
cout << *p << endl; //返回乱码,因为局部变量a已经释放
return 0;
}
一个是堆区,在C++中主要利用new函数在堆区开辟内存
new的语法:(见《C++笔记之内存》)
数据类型 * 变量名 = new 数据类型;
delete 变量名;
数据类型 * 变量名 = new 数据类型(值);
delete 变量名;
数据类型 * 变量名 = new 数据类型[n];//数组,n为数组元素个数
delete [ ] 数组名;
int *func()
{
//new返回的是该数据类型的指针
int * a = new int (10); //a是局部变量,但是保存的是在堆中开辟地址
//使用new去创建一个数组,返回的是数组首地址,开辟的是连续空间
int *arr = new int[5];
for (int i = 0; i < 5; i++)
{
arr[i] = i;
}
for (int j = 0; j < 5; j++)
{
cout << arr[j] << "\t";
}
cout << endl;
delete[] arr; //释放数组时,要加入中括号才可以,不然只释放一个数据
return a; //返回局部变量a指的地址,也就是堆中的地址
}
int main()
{
int *p = func();
cout << *p << endl; //返回10,正确
cout << *p << endl; //返回10,正确,目的是为了与前一个例子栈区做区分
delete p; //堆中数据程序员可以自己释放,如果不释放,程序结束时系统也会释放
return 0;
}
2.引用
2.1 引用的作用:给对象起别名。 语法: 数据类型 &别名 = 原名;
注意事项:引用必须初始化,且初始化引用不可更改。
2.2引用作为函数参数时,可以使得形参修饰实参(引用传递)。
2.3引用作为函数返回值时,不要返回局部变量的引用。(注意:不是不要返回局部变量,而是不要返回局部变量的引用与地址)
int func1()
{
int a = 10;
return a; //返回局部变量a的值
}
int & func2()
{
int a = 10;
return a; //返回局部变量a的引用,也就是说返回的是a本身,即返回的是变量或者说对象
}
//case2、case3正常人应该都不会这样去写,纯属好奇
int main()
{
int p1 = func1(); //case1: 返回值,把值赋值给p
//int &p2 = func1(); //case2:报错,因为函数func1返回的是值
int p3 = func2(); //case3:用a的引用初始化p3,所以后面输出p3正确。虽然正确但一般人也不会这样写,因为既然只要给p3赋值那何必让函数返回引用
int &p4 = func2(); //case4:运行起来乱码,因为a是局部变量,返回a的引用,那么p4是a的别名,但a在函数func2运行结束后就释放了
cout << p1 << endl; //返回10,正确
cout << p1 << endl; //返回10,正确
//cout << p2 << endl;
//cout << p2 << endl;
cout << p3 << endl; //返回10,正确
cout << p3 << endl; //返回10,正确
cout << p4 << endl; //返回乱码,但是当此行代码位于第24行时。输出正确。因为编译器做了保留
cout << p4 << endl; //返回乱码
return 0;
}
2.4 当函数的返回值是引用时,函数的调用可以作为左值。
int & func2()
{
static int a = 10; //a为静态变量。在全局区里,在程序结束时由系统释放
return a; //返回静态变量a的引用
}
int main()
{
int &b = func2();
cout << b << endl; //返回10,正确
cout << b << endl; //返回10,正确,因为此时b就是a的别名,并且a没有因为函数func2的结束而被释放
func2() = 100; //如果函数返回的是一个引用,那么这个函数的调用可以作为左值(也就是赋值语句的左边)。
cout << b << endl; //返回100,正确
cout << b << endl; //返回100,正确
return 0;
}
但是返回const 修饰的引用是不可当左值的!!!
int &func()
{
static int a = 10;
return a;
}
const int &func1()
{
static int a = 10;
return a;
}
int main()
{
int b = 20;
cout << func() << endl; //10;
func() = b;
cout << func() << endl; //20;
//func1() = b; //报错
//cout << func1() << endl;
return 0;
}
2.5引用的本质:引用的本质在C++内部的实现是一个指针常量。
细节就是 int & ref = a;编译器就转换成 int * const ref = a; 遇到ref 时就自动解引用变成 *ref (会用引用就行,知道实现的本质是指针常量就行,并且推荐使用引用,特别是在值传递时,复制效率低,而使用引用就不需要拷贝,节省资源)
2.6 常量引用:修饰形参防止误操作,也就是防止形参修改实参。
void ShowNumb(const int & b) //用const修饰是为了避免实参被修改
{
cout << b << endl;
}
int main()
{
int a = 10;
//int &b = 100; //报错,因为引用必须引一块合法的内存空间,引用的作用是给对象起别名
//const int & b = 100;//这里正确是因为加入const后代码变成 int temp = 10; const int & ref = temp; 除了加const还有函数返回引用时也可以当左值
//b = 1000; //报错,因为由const不可修改。
//cout << b << endl;
ShowNumb(a);
return 0;
}
引用总结:
1>引用的作用就是给对象起别名
2>引用的好处是避免了拷贝,节省资源,一般用在希望函数修改形参以及传递class object时
3>引用是通过指针常量实现的
4>函数返回引用可以当左值但是被const修饰的引用不可以作为左值
5>int & a = 10;是错误的。理由如上2.6。除了加const后还有函数返回引用时可以,理由如上2.4。
6>引用常量可以防止误操作,变成只读,同时又具备避免拷贝的好处
7>不可返回局部变量的引用与地址
3.函数高级用法
3.1 函数默认参数。在C++中允许函数的形参列表中的形参是可以有默认值的。
语法:返回值类型 函数名(参数 = 默认值){}。如果我们自己传入数据,就用自己的数据,如果没有那就用默认参数。
注意:如果某个位置有默认参数,那么从这个位置开始向右都要有默认参数。(也就是说默认参数前面可以空,但后面的必须排满)
错误:void function1(int a, int b= 1; int c ){}
正确:void function2(int a, int b= 1; int c= 2){}
如果函数的声明有默认参数,那么函数的实现不能有默认参数。声明与实现只能有一个有参数。(也不可能出现声明与实现各有一半默认参数的情况,因为这肯定不符合左边有默认参数,右边一定有的规则)
默认参数总结:规则1:一定从最右边开始。规则2:函数定义与函数声明中只能由一个地方出现默认参数,一般选择函数声明处
3.2函数占位参数。 语法: 返回值类型 函数名(参数类型){}
目前还用不到占位参数,后面会涉及到,当我们重载后置递增递减运算符时为了区分前置就增加一个占位符以示区分。
占位参数还可以有默认参数。例如: void func(int = 10){}
占位参数的加入可以发生重载。
占位参数也是需要传入参数的。
3.3函数重载。 作用:函数名可重复,提高复用性。
函数重载满足条件:同名不同参(不同参可以是参数类型不同;参数个数不同;参数顺序不同),且要在同一作用域下(其实我们往往不会在局部去声明函数,一般都是全局,基本上都满足此条件,详情见《C++ primer》第五版P210)
注意:如果仅仅是返回类型不同,则构不成函数重载,而且这样会造成二义性(不知道调用哪个),其实说白了,函数重载与返回类型无关,不管返回类型变不变化都不影响函数重载。
引用作为重载的条件。加入引用也是重载,在引用的基础上再加个const又是一个重载。唯一需要关注的是二义性,即不能让形参赋值同时满足多个函数。
//void func(int a)
//{
// cout << "func(int a)" << endl;
//}
void func(int & a)
{
cout << "func(int & a)" << endl;
}
void func(const int & a)
{
cout << "func(const int & a)" << endl;
}
int main()
{
int b = 10;
//func(1); //func(int a)与func(const int & a)重复,有二义性
//func(b); //func(int a)、func(int & a)与func(const int & a)重复,有二义性
//注释void func(int a)
func(1); //调用func(const int & a)。理由:int & a = 1报错,const int & a = 1
//相当于int temp = 1; const int & a =temp,
//其实就是加了const后先将1赋值给中间变量temp,再把temp赋值给cosnt int & a
func(b); //调用func(int & a)。理由:b是一个变量,可读可写
return 0;
}
重载碰到默认参数,很有可能出现二义性,要尽可能避免。
例如:
void func (int a, int b=10){}
void func (int a){}
int main()
{
//func(1); //报错,出现二义性
func(1,3); //成功
return 0;
}
3.4函数内的局部静态成员
一般和类中静态成员使用方法类似,就是在函数内创建一个静态成员,每次调用该函数,里面静态成员的值就是上一次调用后的值。不像一般的成员,调用完就销毁,再调用再创建。
3.5内联函数(次要)
在函数前加inline关键字就是内联函数,作用就是不再调用该函数,而是在调用该函数的位置使用这函数的副本替换,以增加一些效率,并且一般只能是不复杂的代码可以写成内联函数,像循环等是玩玩不可写成内联,最后,写成内联函数也仅仅只是对编译器提出一种要求!
3.6函数指针
1>概念:函数指针是一个指向函数的指针,与函数的返回类型与形参类型有关,与函数名无关。
示例:
bool MyCompare(int a, int b); //函数声明
bool(*pf) (int a, int b); //函数指针pf
注意:这里*与指针名一定要用括号括起来,不然这又是一个函数声明,返回的是指针。
2>使用:函数名当作值使用,那么函数会自动转换成函数指针。而且加不加引用都可以。同时,也可以使用函数指针去调用该函数,而且也不要解引用。
bool(*pf) (int a, int b); //函数指针pf
pf = MyCompare; //pf = &MyCompare;也可以
if (pf(2, 5))
{
cout << "2大于5" << endl;
}
else
{
cout << "2小于5" << endl;
}
3>如果函数有重载,那么只要保证函数指针的参数与重载的函数匹配即可(不管是参数列表还是返回类型)
示例:
bool MyCompare(int a, int b);
bool MyCompare(int a, int b, int c);
bool(*pf1) (int a, int b) = MyCompare; //显然指向第一个
//int(*pf2) (int a, int b, int c) = MyCompare; //报错,没有匹配的
4>函数指针作为形参
void func1(int(*pf)(int a, int b));
使用时直接把函数名当作实参传到func1中即可。
void func1(int(*pf)(int a, int b));
int f(int a, int b)
{
……
}
void test()
{
func1(f);
}
当然可以使用关键字typedef 简化类型,详情见《C++primer》P222
5>返回函数指针
bool(*f1(int a))(int a1, int a2);
函数f1的形参为a,返回类型为指向返回类型为bool参数列表为(int,int)的函数的指针