C++: Essential C++ 读书笔记(chapter2):面向过程编程:调用函数。

1:传值和传址区别

按值传递:

  • 在调用函数中将原函数的值拷贝一份过去给被调用的函数,在被调用函数中对该值的修改,不会影响原函数的值。
  • 值传递,变量赋值,修改变量的值------修改的是新地址(复制地址)的内容,并不会影响到原来地址的数据。 
  • 在函数调用中,形参是实参内容的拷贝,并不是地址的拷贝,所以改变形参的值,并会影响实参的值。

按地址传递:

  • 在调用函数的时候将原函数的值所在的地址拷贝一份过去,被调用函数对这个地址所作的修改会影响原理的值。
  • 地址传递:用指针修改变量的值,就是把原地址中的值修改了,就相当于对实参本身进行的操作。

1.1:概述:变量的地址和地址中的内容 

首先我们要知道:变量a的地址和 变量a地址中的内容的区别,数据是存放在内存中的,每一个变量都有一个内存地址,变量的内容存放在对应内存地址的空间中。

如:定义  :

int a = 10  ;

那么a在内存中的地址是 ox1100 , 在这个地址中存储的数据就是 10 。

 

假设创建指针 p ,把a的地址赋值给 p ,就是把 a的首地址 ox1100 赋值给 指针p ,这个时候p的值就是变量a在内存中的首地址。

int a = 10;

int *p;

p = &a;

1.2 :案例:值传递  

新开辟一个内存空间,存储原理变量的值,修改变量修改的是新内存空间中的值,所以,原始的参数是不会被函数修改的。

优点: 通过值传递传的参数可以是内置类型参数:数字,字符等。原本参数的值是不会被修改的。

缺点:不能修改原参数的值。

#include<iostring>
#include<string>
uising namespace std;

void swap(int x, int y) {

    int temp;
    temp = x;
    x = y;
    y = temp;
}

int main(){

    int a = 4, b = 6;
    printf("交换前:n  a = %d, b = %d\n", a , b);
    swap(a,b);
    printf("交换后:n  a = %d, b = %d\n", a , b);
    return 0;
}

1.3:  址传递 

址传递:就是指针传递,形参实际是指向实参地址的指针,当对形参进行操作时,就相当于对实参本身进行操作。

可以改变指针指向内容的值,但是不能改变指针本身的地址。

#include<iostream>
#include<string>
using nameapace std;

void swap(int* x, int* y) {

    int temp = *x;
    *x = *y;
    *y = temp;
}

int main(){
    int a = 4,b = 6;
    printf("交换前:a = %d ,b = %d", a, b);
    swap(&a, &b);
    printf("交换后:a = %d ,b = %d", a, b);
}

1.4:案例:面试题 :看下面面试题

#include<iostream>
#include<string>
using namespace std;

void getMemory(char*  p) {

    p = (char*) malloc(100);
}


int main(){
    char*  str = NULL;
    getMemory(str);
    // 运行会报错:程序会崩溃,没有输出(因为str 始终是 NULL)
    strccpy(str, "hello wodl");
    printf(str);
}
  • 解析:函数getMemory()  并不能动态传递内存,main函数中的 str 一直是 NULL,strcpy(str, "hello world" ); 将使程序崩溃,无法输出。
  • 其实函数getMemory()的形参p 是实参数str的一份拷贝,函数中的操作都是针对 p指向的内存地址进行的,实参之前 指向的内存地址是NULL, 后面仍然是 NULL ,所以输出 *str的值就会产生崩溃

解决方案

  • 如要要解决上述问题:我们可以将形参从 一重指针修改为 二重指针。
  • “二重指针指向一重指针的地址”: 也就是说:传递过来的即使就是 *str本身 
#include<iostream>
#include<string>
using namespace std;

void getMemory(char**  p) {

    p = (char*) malloc(100);
}


int main(){
    char*  str = NULL;
    getMemory(str);
    // 运行会报错:程序会崩溃,没有输出(因为str 始终是 NULL)
    strccpy(str, "hello wodl");
    printf(str);
}

2:函数的作用域和范围

案例:return 不可返回指向栈内存的指针。

#include<string>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

// https://blog.csdn.net/qq_43331089/article/details/127529099
// 问题: return 不可返回指向栈内存的指针

vector<int> *fibon_seq(int size) {
	if (size <=0 || size > 1024)
	{
		cout << "waring: fibon_seq(): " << size << "not supported ----reseting to 8\n";
		size = 8;
	}

	vector<int> elems(size);
	for (int i = 0; i < size; i++)
	{
		elems[i] = i + 1;
	}
	cout << "local elems address: " << &elems<< endl;
	return &elems;
}

int main() {
	vector<int> *v = fibon_seq(10);

	// 此处 指针v 保存的内存地址就是函数: fibon_seq()局部变量elems 内存地址
	cout << "local elems address 1: " << v;

	//  error: address of local variable 'elems' returned [-Wreturn-local-addr]
	int a = v->at(0); 
}

 

1: vector<int> *v = fibon_seq(10);  指针v 保存的内存地址就是函数: fibon_seq()局部变量elems 内存地址 。

 2:但是局部变量在函数fibon_seq()执行完毕后,就会释放。

3:所以v->at(0)  再从这个地址取数据,由于空间被释放,所以会出现异常。

解决方案 

1:我们可以使 fibon_seq()函数返回 一个 vector 对象

2:返回的这个对象其实是: fibon_seq()函数局部变量 elems 副本

3:函数执行完毕 源对象:elams被回收,副本对象仍然保留。

2.1:简述:return不可返回指向栈内存的指针原因以及修改方案。 

数据报错在静态存储区与动态存储区的区别就是:静态存储区在编译---链接阶段就已经确定,程序运行过程中不会在变化,只有当程序退出的时候,静态存储区的内存才会被系统回收,动态存储区是在程序运行过程中动态分配的。

而静态存储区包括:文字常量区,全局数据区,静态变量等等,都归静态存储区这一大类。

 举例:先看一下return返回指向栈内存指针的例子:

char* GetStr() {
	// 保存在栈中
	char p[] = "hello";
	return p;
}

int main() {
	char* str = NULL;
	str = GetStr();
	printf("%s\n", str);
}

 $ g++ -c main.cpp -o main.o
main.cpp: In function 'char* GetStr()':
main.cpp:28:7: warning: address of local variable 'p' returned [-Wreturn-local-addr]
  char p[] = "hello";
       ^

编译运行,可以看到如上警告。

和我们预期输出字符串  Hello 并不相符。

原因:

1:因为 Getstr 函数返回指向栈内存的指针,这里的变量 p是局部变量,而局部变量时分配在栈上的。

2: Hello 字符串即保留在栈内存上,栈内存在函数调用结束会自动销毁,因此此时的 p 里的内容是未知的,所以结果无输出。

2.1.1  修改方案 :将Hello 存储在静态区(常量区)

const char* GetStr() {
	// 保存在栈中
	const char *p = "hello";
	return p;
}

int main() {
	const char* str = NULL;
	str = GetStr();
	printf("%s\n", str);
}

// 打印结果 :Hello

问题: 同样是字符串 为什么一个在栈上,一个在静态区。

1: char *p = "Hello"  ----- 首先定义了一个指针变量 p , 编译器就会为指针变量开辟栈空间,但是此时并没有空间来存在 Hello  , 所以 Hello 只能存储在静态区。

2:char p[] = "Hello"  -----此处首先定义一个数组 p ,因为未给出数组大小,所以数组大小未确定,然后把 Hello 保存在这个数组里,编译器就会为数组 p 开辟适当的空间来存储 Hello  

2.2:修改方案:将 p定义成全局变量 

因为全局变量存储在静态存储区,程序结束后才会释放,但是这样会导致函数是不可重如的。

int* p = new int;
int* GetInt() {
	*p = 1;
	return p;
}

int main() {

	int* i = GetInt();
	printf("%d\n", *i);
}

// 打印内容: 1

 2.3:修改方案:使用malloc申请动态内存

在 Getstr函数中使用 malloc 申请动态内存后,使用完毕需要通过 free进行释放,否则会导致内存泄漏。

2.4:修改方案:将变量p声明为static 静态变量。 

char* Getstr2() {
	static char p1[] = "hello111";
	return p1;
}

int main() {
	const char* str = NULL;
	str = Getstr2();
	printf("%s\n", str);
}

// 打印结果: hello111

3:动态内存管理

1:栈又叫堆栈 -----》非静态局部变量/ 函数参数/ 返回值等, 栈是向下增长的

2:内存映射段是高效的 I/O映射方式,用于装载一个共享的动态内存库,用户可使用系统接口创建共享内存,做进程间的通信

3:堆用于程序运行时动态内存分配,堆是可以上增长的

4:数据段----存储全局数据和静态数据

5:代码段----可执行代码/ 只读常量。 

3.1:new/ delete 

int main()
{

    int* p1 = new int;   // 在堆区申请一个 int大小的空间,不会初始化
    int* p2 = new int(0)   // 在堆区并初始化为0 

    delete p1;
    delete p2;

    int* p3 = new int[10]   // 在堆区申请一块10个int 大小空间,未初始化
    int* p4 = new int[10]{1,2,3,4}  // 初始化为 {1,2,3,4,0,0,0,0,}
    delete[] p3
    delete[] p4
 }

new/delete 和 malloc/ free 区别

1:对于内置类型,没有区别

2:new 和delete是C++的关键字/ 操作符,而malloc和free 是C语言的库函数

3:对于自定义类型,相比于malloc 和free ,new和delete会额外调用类中的构造函数和析构函数

4: malloc 的返回值是 void*  使用时需要强转,new 后边跟的是空间的类型,所以new不需要强转。

5:malloc 失败返回空指针,需要判空;new失败抛异常,需要捕获异常。 

3.2 new的原理 、delete原理

new等价于  operator new()+ 构造函数。 operator new() 不是new 运算符的重载,它是库里的一个全局函数。

其中 operator new 是对malloc 的封装,如果malloc 失败,将会抛出异常。

void *_CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{

    void *p;
    while ((p = malloc(size)) ==0) {
        if(_callnewh(size) ==0) {
            
             // report no memory
             // 如果申请内存失败了,这里会抛出 bad_alloc 异常
             static const std::bad_alloc nomem;
             _RAISE(nomem);
        }
    }
    return (p);
}

delete 等价于 operator delete()+ 析构函数 :

  • 从底层代码可以看出 operator delete()调用了 free。
  • 所以针对内置类型或无资源的类对象 delete时,使用delete和free效果相同。但是对于有资源需要释放的对象时,直接使用free虽然也释放了对象的空间,但对象内部的资源还未被清理,会导致内存泄漏,这种情况必须使用 delete。
// operator delete : 该函数最终是通过 free来释放空间的
void operator delete(void* pUserData)
{

    _CrtMemBlockHeader*  pHead;
    RTCCALLBACK(_RTC_Free_hook,(pUserData,0));

    if (pUserData ==NULL) {
        return;
    }
    _mlock(_HEAP_LOCK);  // block other threads
    _TRY

    // get a pointer to memory block header
    pHead = pHdr(pUserData);

    // verify block type
    _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead -> nBolckUse));
    
    // 调用 free()
    _free_dbg(pUserData, pHead->nBlockUse);
    _FINALLY
    
     // release other threads
    _munlock(_HEAP_LOCK);
    _END_TRY_FINALLY
    return;
}

3.3:  new T[N]原理 和 delete[]原理

new T[N] 原理

  1. new T[N] 调用 operator new[]
  2. operator new[] 调用 operator new 完成N个对象空间的开辟
  3. 调用N次构造函数完成N个对象的初始化。

delete[] 原理

  •      调用N次析构函数完成 N个对象资源清理的工作
  •      调用 operator delete[] 
  •      operator delete[] 调用operator delete完成整段空间的释放

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值