1. 函数(方法),是程序最小的算法组织单元,将程序一个大的算法进行分解成一个个函数的小算法叠合而成,不管什么程序,程序的执行过程都是函数相互嵌套调用的过程。
在面向对象的语言中,本质还是函数和方法,类只是一种以对象进行管理和编程的手段。
2. 程序分解成函数的好处
(1)代码重复利用
(2)节省存储空间
(3)便于模块设计,提高开发效率
但是不能将算法无休止的分解成为各种函数模块,注意把握一个度,不然不但没有
提高编程效率,反而还较低了效率。
3. 全局函数和成员函数(方法)
(1)c中,所有的函数都是全局函数,全局函数涉及链接域的问题
(2)c++中,包含了C中的全局函数(有链接域),也包含了类中的成员函数
本章后续讲解中提到的函数,默认为全局函数。
4. 函数的定义和函数原型声明
(1)函数定义
(1)函数头:函数名尽量以动词开头,表明函数动作
(2)函数体
(2)函数声明函数头;
(1)在函数原型中尽量包含参数名,参数名能够起到很好的说明作用,在编程
风格良好的代码中,函数原型中的的参数名称具有极强的说明作用。
(2)函数定义本身也是一种声明,与变量的声明类似,也是有关强弱符号的问题,
主要就是为了在程序进行链接的时候,便于函数链接域的扩展。
(3)函数声明尽量放在头文件中,头文件的作用就是用于暂存各种声明
(1)预处理类的声明
(2)各种自定义类型的声明:结构体,联合体,类
(3)各种函数声明
(4)内联函数的定义
(5)全局变量的声明
(3)C语言中,不能有同名的全局函数,但是在C++中,可以有同名的函数,只要参数
列表不同即可,在c++中,域名+函数名+参数列表来表示一个唯一的函数签名。
5. 参数和变元(实参)
(1)c与c++中函数调用
传参和返回值都允许进行隐式的强制转换,但是很有可能会出现数据丢失的
情况,所以要求尽可能将实参与形参的类型进行统一,必要时可以使用()
和reinterpret_cast<>()等强制转换符号将实参进行显示的转换。
(2)函数参数传递的几种情况
(1)普通值传递(C)
(1)好处,防止修改
(2)缺点,传递结构体和数组组合类型等效率低
(2)特殊值传递(指针或地址)(C)
(1)好处效率高,函数可以修改变元(实参),对于数组/结构和对象
来说,很合适使用指针传递
(2)缺点,可能不小心会修改实参,这个时候就需要const进行修饰
(3)指针传递中经典案例:一维数组和多维数组的传参
(3)使用引用进行传参(c++才有的方式)
(1)效率最高的传参方式,实参和形参是同一个,不会复制,没有副本空间
(2)引用的负面问题
传递引用时实参的写法与传递一个普通值得写法相同,导致调用该函数时
很难区分实参的值会不会修改,非常不利于程序代码的清晰阅读。
(3)如果函数形参是非常量引用的话,不允许实参是常量(或者是const,或者是这类的数值常量(宏常量))
(4)使用引用进行传参是为了提高了效率,如何防止实参被修改的情况呢?使用const,如果形参
是const引用,实参为常量或者变量都可以
(5)引用只能被初始化,不能被赋值,而且编译器要求引用必须被初始化。传递引用,其实就是初始化引用,
每次函数被调用时,引用类型的形参都会被重新初始化。
(3)指针和引用
(1)引用的效率会高于指针
(2)不管是使用引用还是使用指针,只要不涉及修改实参的操作,就一定要使用const进行限制
(3)指针类型的形参是一个新的变量空间,使用时需要使用*解引用(寻找指向的空间)。
引用只是一个别名,形参和实参是同一个变量空间,使用时不需要解引用。
(4)指针可以为NULL,但是引用一般情况不许为NULL(除了NULL的引用),因此使用
指针前,最好是对指针先进行是否为空的判断,这在前面讲指针的时候特意提到过。
特别是在返回指针做回左值时,一定要保证指针的有效性。
6. main函数
(1)main函数被谁调用的
被启动代码,在编译时,编译器会自动加入启动代码,启动代码有系统(OS)加
载器调用。
(2)main函数的两个参数
main函数的两个参数用于接收命令行输入的参数,在windows或者ubuntu下
输入命令行时,有输入参数的需求,所以我们应该理解,执行的各种命令
其实就是一个可执行程序。
命令行执行程序时,至少有一个参数,那就是程序名自己。
(1)argc:argument count,命令行输入的参数个数
(2)argv:argument vector,一个字符串指针数组,存放命令行输入的参数(字符串)的地址
测试用例
using namespace std;
void fun(const int &argc, const char ** &argv) {
//for(int i=0; i<argc; i++) {
for(int i=0; argv[i] != NULL; i++) {
cout<< argv[i] << endl;
}
}
int main(int argc, char **argv)
{
fun(argc, argv);
return 0;
}
7.给函数指定默认参数值
注意,在c中不支持默认参数设置。
(1)形参的给默认参数值从右边向做左边逐个给值,要求连续,不能跳跃
(2)如果函数只有定义没有声明,就将默认参数值盖在定义中,如果有函数声明
就将默认参数值给在函数声明中,定义中就不要再给默认值。
(3)默认值的好处是,如果不给传参时,函数可以使用默认数值,这个在c++中的构造函数中经常使用这样的操作。
(4)当使用默认形参参数值时,调用者可以从右向左连续省略部分函数参数。
(5)形参为引用时,也是可以为其指定默认值
int aa = 200;
void fun(int a, int b=10, int &c=aa) {
printf("a = %d, b = %d\n", a, b);
printf("c = %d\n", c);
}
int main(void)
{
fun(1);
return 0;
}
8. 函数返回值
(1)如果返回值不是void,必须要使用return语句,否者可写可不写。
(2)函数返回值使用情况
(1)可以舍弃
(2)如果返回的是一个普通值(非指针和引用类的值),都作为左值使用
(3)如果返回值为一个引用或者指针时,可以作为左值使用,也可以作为
右值使用,但是指针作为右值使用时,必须解引用,而且指针一定
不能为空。如果返回的指针和引用是常量时,注意是不能进行赋值修改。
测试用例
(3)函数返回值必须注意的情况
(1)不能返回自动局部变量,当然相反就可以返回静态局部变量
(2)不能返回自动局部变量的引用,相反可以返回静态局部变量的引用
测试用例
返回一个字符串指针和返回一个局部字符串数组
9. 在函数中返回分配的自由存储空间(手动存储区)的地址
(1)这个在讲链表的节点创建时,已经使用过了。
10. 全局函数函数链接域(作用域)的问题
类的成员函数在讨论范围外,这里只讨论全局函数。
(1)默认定义函数时前面都有一个extern关键字,表示函数是外链接的,表示作用域扩展到了
其它的文件中,但是其它的文件在使用时,需要进行声明,函数定义是强符号,声明是弱符号,
在相同的命名空间中,强符号只能有一个,声明(弱符号可以有多个),所以在整个全局
命名空间中,函数定义只能有一个,声明有多个,将强弱符号统一的过程就是链接函数的过程。
(2)将extern改为static修饰函数
当c或者c++程序实现的程序超过一定体量之后,如果我们将所有的全局函数都命名在全局命名
空间的话,换句话说让全局函数都具备外链接属性时,不可能不会遇到命名冲突的函数,那怎
么办呢,我们往往都会将那些不需要在其它文件的被访问的函数使用static修饰,将其链接属
性改为内链接,这样一来开,即便是其他文件的名字与本文件某个static修饰的文件的名字冲
突了,也不会有任何问题。
对于全局变量来说也有着与函数一样的情况,这一点在后面继续讲解。
11. 内联函数
(1)函数的问题
实际上函数的调用的开销是比较大的,主要是需要开辟函数栈空间存放函数状态和返回地址,
当然还有各种局部变量,但是当一个函数不涉及循环,而且代码量非常短,在5句话以内,
而且函数调用还非常的频繁,这样的函数调用的效率是很低的。
(2)早期使用带参宏定义来解决前面提到的问题
#include <iostream>
#include <string>
using namespace std;
#if 0
static int compare_fun(int a, int b)
{
return a>b ? a : b;
}
#endif
#define compare_fun(a, b) ((a)>(b)?(a):(b))
int main(int argc, char **argv)
{
cout << compare_fun(10, 6) << endl;
return 0;
}
宏定义之所以能够解决这样的问题的原因是,宏展开后,代码直接被填充在了宏被调用的位置
,实际上不存在函数调用的开销。但是使用带参宏的问题是,宏定义不会对参数进行参数类型的
检查,不利于代码排错(可以想见,在强类型的语言中,类型参数在编译检查时是多么重要)。
(3)内联函数的出现
为了解决上述矛盾,寄希望代码想宏定义一样实现替换提高效率,也希望像函数一样能够实现
参数的类型检查,C语言在后来提出了内联函数,内联函数结合了函数和宏的双从特点。
#include <iostream>
#include <string>
using namespace std;
static inline int compare_fun(int a, int b)
{
return a>b ? a : b;
}
int main(int argc, char **argv)
{
cout << compare_fun(10, 6) << endl;
return 0;
}
(4)内联函数的一些需要注意的问题
(1)c和c++都有内联函数
(2)对于C和C++全局函数而言,只有给了inline关键字修饰后的才是内联函数。
(3)但是在C++中,需要注意,如果成员函数直接定义在类的内部的话,不管使不使inline关键字的话
都是内联函数,当然编译器自己再做一次判断,该函数适不适合编译成内联函数。但是如果只
是将成员函数的声明放在了类里面,函数的定义放在了外部的话,只有给函数加上inline关键字
,该函数才会变成内联函数,在后续的课程中还会再次讲这个问题。
(4)内联函数一般都是写在头文件中
实际上多有的函数定义都可以写在头文件中,但是实际情况是,我们所有的全局函数的定义都是
定义在cpp文件中,只是将声明放在了头文件中,因为具有外部链接(A文件定义,B文件可以使用)
的函数在全局作用域中只能有一个函数的定义,如果将普通全局函数的定在了头文件中的话,会导
致同一个函数被多次定义的情况,编译器是不允许的。
总结:在同一个编译单元中,类/结构体等的类型定义,以及内联函数的定义,在同一个编译单元中只能
有一次,但是可以同时出现在不同的编译单元中。
但是对于普通全局函数定义和全局变量定义(强符号),在全局名中定义只能有一次,但是声明
可以有多个,也就是说除了允许在一个编译单元内重复外,也不允许在多个编译单元中重复定义。
例子1:
static inline int compare_fun(int a, int b)
{
return a>b ? a : b;
}
static inline int compare_fun(int a, int b)
{
return a>b ? a : b;
}
int main(int argc, char **argv)
{
cout << compare_fun(10, 6) << endl;
return 0;
}
例子分析:在这个文件中,内联函数多次定义出错
例子2:
a.h文件
#ifndef H_A_H
#define H_A_H
int compare_fun(int a, int b)
{
return a>b ? a : b;
}
#endif
a.cpp文件
#include <iostream>
#include <string>
#include "a.h" //------------------------
using namespace std;
int main(int argc, char **argv)
{
cout << compare_fun(10, 6) << endl;
return 0;
}
b.cpp文件
#include <iostream>
#include <string>
#include "a.h"//------------------------
using namespace std;
int fun()
{
cout << compare_fun(10, 6) << endl;
return 0;
}
编译:g++ a.cpp b.cpp
编译错误:multiple definition of `compare_fun(int, int)'
例子分析:
在本例中,比较函数int compare_fun(int a, int b)是一个全局作用域的函数(没有static限制),
而且将其定义写在了a.h头文件中。当a.cpp包含这个头文件时,a.cpp中会定义一个这个全局函数,
当b.cpp包含b.h这个头文件时,也会在b.cpp中也会定义一个这个全局函数。g++对这两个文件进行
编译链接时,整个程序中会出现来两份全局的compare_fun函数,我们前面讲过,在程序中是不允许
出现多个全局函数的重复定义的,因此会报错误。
因此对于全局函数来说,在整个程序中定义只能有一份,其它文件需要使用时,只需要包含其声明就可以了。
本例如何修改:
(1)方法1:int compare_fun(int a, int b){ ...... }函数改为static int compare_fun(int a, int b){ ...... }
将其链接域改为内链接,其不再是全局可见,这样一来,a.cpp和b.cpp中的compare_fun函数,是属于
各自文件范围内的函数,不会冲突,通过前面的讲解,这一点应该很好理解。
(2)方法2:将int compare_fun(int a, int b){ ...... }函数改为inline int compare_fun(int a, int b){ ......}
因为内联函数与普通的函数不一样,内联函数在整个程序中可以有多份定义,不会报错重复定义的错误,
但是必须注意的是,在同一个文件中,同一个内联函数的定义只能有一份,这在前面已经讲过了。
12. 静态局部变量
(1)
(2)内存的各种不同的管理方式
(1)栈
(1)栈:也成为自动变量区
(2)用于开辟函数栈
(3)自动局部变量(普通类型的,类类型的对象)都是开辟与栈中
int fun() {
(auto) int a = 100;
string str("hello");
}
(4)生命周期:函数栈的生命周期与函数执行的时间同,函数执行完毕,函数栈释放后,开辟于栈中的
变量空间就被释放
(5)作用域:自动局部变量,从定义的位置开始到函数结尾,不能通过声明去修改其作用域
(2)堆
(1)也被称为自由存储区,或者叫手动存储区
(2)malloc和new所开辟的内存空间均来自于堆空间,需要使用free和delete释放
int fun() {
int a = new int;
int *p = (int *)malloc(sizeof(int));
string *str = new string("hello");
}
(3)生命周期:开辟空间开始到释放空间期间有效
(5)作用域:与指向对空间的指针的作用域有关,就看指针是全局的还是局部的
(3)静态区
(1)静态数据区
(1).bss:开辟没有初始化的静态变量(全局变量和静态局部变量)空间,会自动初始化为0
(2).data:开辟初始化了静态变量空间(全局变量和静态局部变量)
(3).rodata:存放常量,比如字符串常量就是存放在这个区域中
(2)静态代码区
(1).text:存放代码
12. typedef与函数之间
(1)在真实的c语言实现的大型工程项目中,往往会使用typedef以及宏定义实现复杂的符合表达式
当然这些表达式往往让人理解起来很困难。
C++函数
最新推荐文章于 2023-03-30 15:52:14 发布