【Essential C++学习笔记】第二章 面向过程的编程风格

第二章 面向过程的编程风格

将函数独立出来的做法可带来三个主要好处:第一,以一连串函数调用操作,取代重复撰写相同的程序代码,可使程序更容易读懂。第二,我们可以在不同的程序中使用这些函数。第三,我们可以更轻易地将工作分配给协力开发团队。

2.1 如何撰写函数

函数必须先被声明,然后才能被调用(被使用)。函数的声明让编译器得以检查后继出现的使用方式是否正确----是否有足够的参数,参数型别是否正确等等。函数声明不必提供函数的主体部分,但必须指明返回型别、函数名称,以及参数表.此即所谓的函数原型

函数参数的三种传递方式——值传递、指针传递、引用传递

在函数参数中一会用 *,一会用&,有点莫名其妙,下面看看是什么个情况

(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); 

2.2 取地址运算符 ( & ) 和 间接寻址运算符 ( * )

& 返回操作数的内存地址。如果 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; 
}

2.3 左值右值的理解

左值表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象
右值不表示内存中某个可识别位置的对象的表达式。

int var;
var = 4; //其中 var 是一个有内存位置的对象,因此它是左值

取地址操作符 '&' 需要一个左值参数,返回一个右值:

int var = 10;
int* bad_addr = &(var + 1); // 错误: 一元 '&' 操作符需要左值参数
int* addr = &var;           // 正确: var 是左值
&var = 40;                  // 错误: 赋值操作的左操作数需要是左值

2.4 提供默认参数值

第一个规则是,默认值的决议(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;
}

static关键字

生命周期与作用域

• 生存周期: 变量从定义到销毁的时间范围。存放在全局数据区的变量的生存周期存在于整个程序运行期间,而存放在栈中的数据则随着函数等的作用域结束导致出栈而销毁,除了静态变量之外的局部变量都存放于栈中。
• 作用域: 变量的可见代码域(块作用域,函数作用域,类作用域,程序全局作用域)。

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;
}

• 对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;
• 静态数据成员存储在全局数据区。静态数据
成员定义时要分配空间
,所以不能在类声明中定义。
静态数据成员主要用在各个对象都有相同的某项属性的时候。比如对于一个存款类,每个实例的利息都是相同的。所以,应该把利息设为存款类的静态数据成员。这有两个好处,第一,不管定义多少个存款类对象,利息数据成员都共享分配在全局数据区的内存,所以节省存储空间。第二,一旦利息需要改变时,只要改变一次,则所有存款类对象的利息全改变过来了;
• 同全局变量相比,使用静态数据成员有两个优势:

  1. 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;
  2. 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;

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<#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博客

2.9 设置头文件

  • 假如为了不用分别在五个文件中声明seq_elem(),我们把函数声明可以放在头文件中,然后每个程序代码文件(.cpp)文件内#include" "头文件来包含这些函数声明,本意还是为了方便调用一些(自定义)函数,然后也方便修改函数的参表或者返回类型,包含的头文件会自动更新掉。
  • 我们可以 将函数声明连带函数体写入到一个.h文件中,也可以仅在.h头文件中写函数声明,将函数主体放在一个或多个.c文件中。
  • 头文件中常放函数的声明,变量的声明,而不能放它们的定义
  • 为了防止重复编译,在开头和结尾处必须按照如下样式加上预编译语句:“ #ifndef XXX #define XXX 。。。#endif"
//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

你可能感兴趣的:(C++学习,c++,笔记,学习)