将函数独立出来的做法可带来三个主要好处:第一,以一连串函数调用操作,取代重复撰写相同的程序代码,可使程序更容易读懂。第二,我们可以在不同的程序中使用这些函数。第三,我们可以更轻易地将工作分配给协力开发团队。
函数必须先被声明,然后才能被调用(被使用)。函数的声明让编译器得以检查后继出现的使用方式是否正确----是否有足够的参数,参数型别是否正确等等。函数声明不必提供函数的主体部分,但必须指明返回型别、函数名称,以及参数表.此即所谓的函数原型
在函数参数中一会用 *,一会用&,有点莫名其妙,下面看看是什么个情况
(1)值传递
将已经初始化的变量值(或常量)传递到函数中。
int func(int value)
{
int ret = value++;
return ret;
}
//调用函数时,实参需要先进行初始化
int num = 2;
func(num);
值传递是将实参的值赋值给了形参,形如上例中,实际上是:int value; value=num;,所以实参必须要先进行初始化。
另外,因为只是赋值,所以形参value值的改变,并不会影响实参num的值。
(2)指针传递
对于指针传递来说,传递的是指针变量,也是值传递(此时值是指针),是值传递就必须先进行初始化。
1)形参未被初始化的情况(不被允许)
int func(int *value)
{
value = (int*)malloc(sizeof(int));
}
int *p ;//仅定义了一个指针,而未进行初始化
func(p);
由上可见,这种情况下,并不能对实参p进行赋值,因为,这实际上是 值传递(传递的值是指针),而值传递,实参必须先初始化,所以,这种情况是不允许的。但是,指针传递,实参指向的变量可以是未初始化的,即调用函数可以对实参指向的变量进行赋值。这是因为传递的是该变量的地址,所以是有值传递。
// 情况1:实参指向变量为一般变量
int func(int *value)
{
*value =2;
int ret = (*value)++;
return ret;
}
int num;//变量未进行初始化
int *p = #//p为实参,已进行初始化。
func(p);
// 情况2:实参指向变量为指针变量。
int func(int **value)
{
*value = (int*)malloc(sizeof(int));
}
int *p ;//仅定义了一个指针,而未进行初始化
int **q= &p;
func(q);
2)实参已被初始化的情况:
//情况1:实参指向的变量被初始化,但实参p已被初始化
int func(int *value)
{
int ret = (*value)++;
return ret;
}
int num = 2;
int *p = #//p为实参,已进行初始化。
func(p); //num的值增加1
// 情况2:实参指向的变量没有被初始化,但实参p已被初始化 (存储的是所指向变量的地址)
int func(int *value)
{
*value =2;
int ret = (*value)++;
return ret;
}
int num;//变量未进行初始化
int *p = #//p为实参,已进行初始化。
func(p);
(3)引用传递
传递引用就是传递实参本身,此时,实参可以是已初始化,也可以是未初始化的值,引用传递适用于变量中含有的数据很大,传递引用就无需再赋值给形参(相较于值传递)。
// 情况1:非指针实参变量未初始化:
int func(int &value)
{
value = 2;//此处对实参p进行了赋值
int ret = value++;
return ret;
}
int p ;//仅定义了变量,而未进行初始化
func(p); //p的值为3
// 情况2:非指针实参变量已初始化:
int func(int &value)
{
int ret = value++;
return ret;
}
int p = 3;//直接对实参进行初始化
func(p); //执行后p的值为4
// 情况3:指针实参且未初始化的情况:
void func(int* &value)
{
value = (int*)malloc(sizeof(int)*2);//此处对实参p进行了赋值
}
int *p ;//仅定义了变量,而未进行初始化
func(p);
// 情况4:指针实参且已初始化的情况:
void func(int* &value)
{
value[0] = 1;//此处对实参p进行了赋值
}
int *p = (int*)malloc(sizeof(int)*2);//实参初始化
func(p);
& 返回操作数的内存地址。如果 var 是一个整型变量,则 &var 是它的地址。
*****返回操作数所指定地址的变量的值。
声明时,变量前加 “&” :声明引用变量。它是某个已存在变量的别名,即该引用变量名称与原始变量名称都代表同一个变量。
声明时,变量前加 “*” :声明指针变量。它的值是另一个变量的地址。
声明时,变量前加 “*”:声明二级指针变量。它的值是另一个一级"基本类型 *"指针变量的地址 (指针的指针)。
调用时,变量前加 “&” :使用取地址运算符获取该变量的地址
调用时,指针变量前加 “*” :使用间接寻址运算符获取该指针变量所指向的变量
调用时,二级指针变量前加 “**” :获取该二级指针变量所指向的指针所指向的变量
#include
using namespace std;
int main(){
int var; // 声明int类型变量var
int * ptr; // 声明指针变量ptr
ptr = &var; // 先使用 & 运算符获取变量var的地址,再把该地址赋值给指针变量ptr
int ** pptr; // 声明二级指针变量pptr
pptr = &ptr; // 先使用 & 运算符获取变量ptr的地址,再把该地址赋值给二级指针变量pptr
int & ref1 = var; // 声明引用变量ref1, ref1是变量var的别名(引用必须在创建时被初始化)
int & ref2 = *ptr; // 先使用*运算符获取指针变量ptr所指向的变量(即var),再用该变量(var)初始化引用变量ref2(声明引用变量ref2的同时对它进行初始化)。也就是说,该行代码执行后,ref2也是变量var的别名 var = 20
cout << "Value of var: ";
cout << var << endl;
cout << "Value of &var: ";
cout << &var << "\t(var的地址)" << endl;
cout << endl;
cout << "Value of ptr: ";
cout << ptr << "\t(等于&var)" << endl;
cout << "Value of *ptr: ";
cout << *ptr << "\t\t(等于var)" << endl;
cout << "Value of &ptr: ";
cout << &ptr << "\t(ptr的地址)" << endl;
cout << endl;
cout << "Value of pptr: ";
cout << pptr << "\t(等于&ptr)" << endl;
cout << "Value of *pptr: ";
cout << *pptr << "\t(等于ptr, 等于&var)" << endl;
cout << "Value of **pptr: ";
cout << **pptr << "\t\t(等于*ptr, 等于var)" << endl;
cout << "Value of &pptr: ";
cout << &pptr << "\t(pptr的地址)" << endl;
cout << endl;
cout << "Value of ref1: ";
cout << ref1 << "\t\t(等于var)" << endl;
cout << "Value of &ref1: ";
cout << &ref1 << "\t(等于&var)" << endl;
cout << endl;
cout << "Value of ref2: ";
cout << ref2 << "\t\t(等于var)" << endl;
cout << "Value of &ref2: ";
cout << &ref2 << "\t(等于&var)" << endl;
return 0;
}
左值表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象
右值不表示内存中某个可识别位置的对象的表达式。
int var;
var = 4; //其中 var 是一个有内存位置的对象,因此它是左值
取地址操作符 '&'
需要一个左值参数,返回一个右值:
int var = 10;
int* bad_addr = &(var + 1); // 错误: 一元 '&' 操作符需要左值参数
int* addr = &var; // 正确: var 是左值
&var = 40; // 错误: 赋值操作的左操作数需要是左值
第一个规则是,默认值的决议(resolve)操作由最右边开始进行.如果我们为某个参数提供了默认值,那么这个参数右侧的所有参数都必须也具有默认参数值才行
第二个规则是,默认值只能够指定一次,可以在函数声明处,亦可以在函数定义处,但不能够在两个地方都指定。那么、我们应该在何处指定参数的默认值呢?
#include
#include
#include
// 默认使用cout输出,当代码为display(vec,ofil); 表示输出到文件上
void display ( vector vec, ostream &os = cout) {
for (int i = 0; i < vec.size(); ++i) {
os << vec[i] << ' ';
}
cout << endl;
}
生命周期与作用域
• 生存周期: 变量从定义到销毁的时间范围。存放在全局数据区的变量的生存周期存在于整个程序运行期间,而存放在栈中的数据则随着函数等的作用域结束导致出栈而销毁,除了静态变量之外的局部变量都存放于栈中。
• 作用域: 变量的可见代码域(块作用域,函数作用域,类作用域,程序全局作用域)。
C++的static有两种用法:面向过程程序设计中的static和面向对象程序设计中的static。前者应用于普通变量和函数,不涉及类;后者主要说明static在类中的作用。
一、面向过程设计中的static
static变量是指静态的变量,不管是在全局还是局部声明的static变量都存放于程序的全局变量区域,所以它的生命周期是从程序开始到程序结束。但是static变量的作用域并不等同于它的生存周期,它的作用域决定于它被定义的位置。
可以认为静态变量 的作用域<=生存周期。
案例1:静态局部变量
#include
using namespace std;
void fn();
int main() {
fn(); // 10
fn(); // 11
fn(); // 12
return 0;
}
void fn() {
static int n = 10; // 第二次运行,不会再次进行初始化
cout << n << endl;
n++;
}
• 该变量在全局数据区分配内存;
• 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
• 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
• 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;
案例2:静态全局变量
//File1
#include
using namespace std;
int k = 10; //定义全局变量,其他文件引用用extern
static int n = 20; //定义静态全局变量
int main() {
cout << k << endl;
cout << n << endl;
return 0;
}
全局变量和静态变量j都存放于程序的全局数据区域,它们的生存周期都是程序的整个运行期,但是n的作用域为全局作用域,可以通过extern在其他文件中使用,而j只能在文件A中使用,例如在文件B中:
//File2
#include
using namespace std;
extern int k;//ok
extern int n;//error: n在文件B中不可见
int main() {
cout << k << endl;
cout << n << endl;
return 0;
}
案例3:静态函数
在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。
定义静态函数的好处:
• 静态函数不能被其它文件所用;
• 其它文件中可以定义相同名字的函数,不会发生冲突;
#include
static void fn();//声明静态函数
void main()
{
fn();
}
void fn()//定义静态函数
{
int n=10;
cout<
静态全局变量 vs 全局变量
生存周期 | 作用域 | 引用方法 | |
---|---|---|---|
static全局变量 | 从程序开始到程序结束 | 被定义的文件 | 本文件直接引用,其他文件无法引用 |
static局部变量 | 从程序开始到程序结束 | 局部作用域 | 局部直接引用,其他作用域无法使用 |
全局变量 | 从程序开始到程序结束 | 全局作用域(只需要在一个源文件中定义,就可以作用于所有的源文件) | 其他文件用extern 等关键字声明要引用的全局变量 |
总结:
全局变量、局部变量、全局静态变量、局部静态变量的区别。要从分配内存的位置和作用域入手来解释。
全局变量,分配的内存在静态存储区内存上面,其作用域是全局作用域,也就是整个程序的生命周期内都可以使用,同时,有些程序并不是由一个源文件构成的,可能有许多个源文件构成,全局变量只要在一个文件中定义,就可以在其他所有的文件中使用,当然,必须在其他文件使用extern关键字声明该变量。
局部变量,分配内存是分配在栈存储区上的,其作用域也只是在局部函数内,在定义该变量的函数内,只要出了该函数,该局部变量就不再起作用,该变量的生命周期也只是和该函数同在。
全局静态变量,分配的内存与全局变量一样,也是在静态存储内存上,其生命周期也是与整个程序同在的,从程序开始到结束一直起作用,但是与全局变量不同的是,全局静态变量作用域只在定义它的一个源文件内,其他源文件不能使用它。
局部静态变量,分配的内存也是在静态存储内存上的,其第一次初始化后就一直存在直到程序结束,该变量的特点是其作用域只在定义它的函数内可见,出了该函数就不可见了
二、面向对象的static关键字(类中的static关键字)
2.1静态数据成员
在类内数据成员的声明前加上关键字static,该数据成员就是类内的静态数据成员。
#include
using namespace std;
class Myclass {
public:
Myclass(int a, int b, int c);
void GetSum();
private:
int a, b, c;
static int Sum;//声明静态数据成员
};
int Myclass::Sum = 0; //定义并初始化静态数据成员
Myclass::Myclass(int a, int b, int c) {
this->a = a;
this->b = b;
this->c = c;
Sum += a + b + c;
}
void Myclass::GetSum() {
cout << "Sum=" << Sum << endl;
}
int main() {
Myclass M(1, 2, 3);
M.GetSum();
Myclass N(4, 5, 6);
N.GetSum();
return 0;
}
• 对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;
• 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。
• 静态数据成员主要用在各个对象都有相同的某项属性的时候。比如对于一个存款类,每个实例的利息都是相同的。所以,应该把利息设为存款类的静态数据成员。这有两个好处,第一,不管定义多少个存款类对象,利息数据成员都共享分配在全局数据区的内存,所以节省存储空间。第二,一旦利息需要改变时,只要改变一次,则所有存款类对象的利息全改变过来了;
• 同全局变量相比,使用静态数据成员有两个优势:
2.2静态成员函数
与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。
静态成员函数与静态数据成员一样,都是类的内部实现,属于类定义的一部分。普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。
通常情况下,this是缺省的。如函数fn()实际上是this->fn()。但是与普通函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针。从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。
#include
using namespace std;
class Myclass {
public:
Myclass(int a, int b, int c);
static void GetSum();
// 声明静态成员函数
private:
int a, b, c;
static int Sum;//声明静态数据成员
};
int Myclass::Sum = 0; //定义并初始化静态数据成员
Myclass::Myclass(int a, int b, int c) {
this->a = a;
this->b = b;
this->c = c;
Sum += a + b + c; //非静态成员函数可以访问静态数据成员
}
void Myclass::GetSum() { //静态成员函数的实现
// cout<
• 出现在类体外的函数定义不能指定关键字static;
• 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
• 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
• 静态成员函数不能访问非静态成员函数和非静态数据成员;
• 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长;
• 将函数声明为inline,表示要求编译器在每个函数调用点上将函数的内容展开。编译器将该函数的调用操作改为用一份函数代码副本代替,使我们的性能改善,其结果等于把三个函数写入fibon_elem()内,但仍然维持着三个独立的运算单元
• inline
仅仅是对编译器的一种请求而没有强制性,是否执行需视编译器而定。
• 最适合声明为inline的函数:体积小,常被调用,所从事的计算并不复杂。
• inline
函数的定义常被放在头文件中,因为编译器要在它被调用的时候加以展开,此时其定义必须有效。
C++中inline的用法——转自博客园Boblim的博客
1.函数的重载的规则:
2.C++ 是如何做到函数重载的
C++代码在编译时会根据参数列表对函数进行重命名,例如void Swap(int a, int b)
会被重命名为_Swap_int_int
,void Swap(float x, float y)
会被重命名为_Swap_float_float
。当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错,这叫做重载决议(Overload Resolution)。
不同的编译器有不同的重命名方式,这里仅仅举例说明,实际情况可能并非如此。
从这个角度讲,函数重载仅仅是语法层面的,本质上它们还是不同的函数,占用不同的内存,入口地址也不一样。
所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。
function template以关键字template开场,其后紧接成对尖括号<>包围起来的一个或多个标识符,用以表示我们希望推迟决定的数据类型。这些标识符扮演占位符的角色,用来放置函数参数列表及函数体中的某些实际数据类型,用户每次利用这一模板产生函数,都必须提供确实的类型信息。
凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现了不同函数的功能。
函数模板定义形式
由以下三部分组成: 模板说明 + 函数定义 + 函数模板调用
template < 类型形式参数表 >
类型 函数名 (形式参数表)
{
//语句序列
}
详见:https://blog.csdn.net/m0_53636439/article/details/119777817
对于变量我们可以用int *a
这样的语法创建一个指针,如果我们想写一个指向函数的指针我们可以这么写: 类型 ( * name) ();
#include
using namespace std;
int foo() {
return 5;
}
int goo() {
return 6;
}
int main() {
int (* pointFunc)() = foo; //funcPtr 现在指向了函数foo
cout << pointFunc() << endl;
pointFunc = goo; // pointFunc 现在又指向了函数goo
cout << pointFunc() << endl;
return 0;
}
2.把函数作为参数传入另一个函数
例一:
#include
using namespace std;
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
// 传入了一个int型,双参数,返回值为int的函数f
void func(int k, int v, int(*f)(int a, int b)) {
cout << f(k, v) << endl;
}
int main() {
func(2, 3, add);
func(2, 3, sub);
return 0;
}
例二:简单的冒泡排序(可切换升序降序)
#include
using namespace std;
template // 可变类型T
bool upSort(T x, T y) {
return x > y;
}
template // 可变类型T
bool downSort(T x, T y) {
return x < y;
}
template // 可变类型T
// a:原数组 n:数组长度 cmpfunc
void bubblesort(T *a, int n, bool(*cmpfunc)(T, T)) {
for (int i = 0; i < (n - 1); ++i) {
for (int j = i + 1; j < (n - 1); ++j) {
if (cmpfunc(a[i], a[j + 1])) {
swap(a[i], a[j + 1]);
}
}
}
}
int main() {
int a[8] = {5, 2, 5, 7, 1, -3, 99, 56};
int b[8] = {5, 2, 5, 7, 1, -3, 99, 56};
bubblesort(a, 8, upSort);
for (auto e : a)
cout << e << " ";
cout << std::endl;
bubblesort(b, 8, downSort);
for (auto e : b)
cout << e << " ";
cout << std::endl;
return 0;
}
可以设置默认函数
//设置默认函数
void bubblesort(T *a, int n, bool(*cmpfunc)(T, T) = upSort)
关于函数指针和函数指针数组的定义及更多相关内容请看该文章。写的很好
函数指针和函数指针数组及其应用——whuTommy的CSDN博客
//printhello.h
#ifndef PRINTHELLO_H_
#define PRINTHELLO_H_
void printhello();
#endif
// printhello.cpp
#include"printhello.h"
void printhello(){
std::cout << "Hello!\n";
}
//main.cpp
// 调用刚刚创建的头文件,用“”(标准库的用<>),然后使用头文件里面的函数printhello():
#include "printhello.h"
int main() {
printhello();
return 0;
}
""
与尖括号<>
包含的区别:
- 如果头文件和包含此文件的程序代码文件位于同一个磁盘目录下,便使用双引号;如果在不同的目录下,便使用尖括号。
- 如果此文件被认定为标准的或项目专属的头文件,便使用尖括号,编译器会先在某些默认的磁盘目录中寻找;如果文件名由双引号括住,该文件被认为是用户提供的头文件,搜索时从要包含此文件的程序文件所在的磁盘目录开始找起。
关于头文件相关的可以看看下面这个文档进行理解:
函数头文件:https://blog.csdn.net/Leeoo_lyq/article/details/105739741?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-105739741-blog-83017491.235%5Ev38%5Epc_relevant_default_base&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-105739741-blog-83017491.235%5Ev38%5Epc_relevant_default_base&utm_relevant_index=2
类头文件:https://zhuanlan.zhihu.com/p/476285979