C++随笔1 --成员函数、返回引用、运算符重载、内存的堆栈

1.C++静态成员函数和非静态成员函数的区别

数据成员

静态数据成员是类的一部分,为类的所有实例共享(静态区);非静态数据成员,类的每个实例都有一份拷贝(动态区)。

静态数据成员的访问

静态数据成员是类的一部分,在产生任何实例之前已经存在。

函数成员(都在代码区)

静态函数成员与非静态函数成员都为类所有,对象中并不存在函数的拷贝(每个对象所占用的存储空间只是该对象的数据成员所占用的存储空间,但是在逻辑上函数和数据是一起被封装进对象的)。静态成员函数和非静态成员函数的根本区别在于有无this指针。非静态函数由对象名.或者对象指针->调用,调用时编译器会向函数传递this指针;静态成员函数则有类名::或者对象名.调用,没有this指针,不识别对象个体,经常用来操作类的静态数据成员。访问类的非静态成员要通过对象来实现。

内存角度分析:

类的静态成员(数据成员和函数成员)为类本身所有在类加载的时候就会分配内存,可以通过类名直接访问;非静态成员(数据成员和函数成员)属于类的实例所有,所以只有在创建类的实例的时候才会分配内存,并通过实例去访问。

注意:类的静态数据成员是静态存储,它是静态生存周期,必须进行初始化

注意:静态数据成员的初始化在类体外进行,前面不加static以免与一般静态变量或者对象混淆。

静态成员函数访问非静态成员报错:

类的静态成员在类加载的时候就已经分配内存,而此时类的非静态成员尚未分配内存,访问内存中不存在的东西自然会出错。

#include<iostream>
using namespace std;
class StaticTest
{
public:
	StaticTest(int a){A=a;B++;}  // B实际为类实例化的对象的个数
	static void printTest(StaticTest t);
private:
	int A;
	static int B;
};
void StaticTest::printTest(StaticTest t)
{
	cout<<"t.A: "<<t.A<<endl;
	cout<<"StaticTest::B: "<<StaticTest::B<<endl;
	cout<<"t.B: "<<t.B<<endl;
}
int StaticTest::B=0;
int main()
{
	StaticTest a1(66);
	StaticTest::printTest(a1);
	StaticTest a2(88);
	StaticTest::printTest(a2);
	return 0;
}

2.引用作为函数的返回值

语法:类型 &函数名(形参列表){ 函数体 }

特别注意:

1.引用作为函数的返回值时,必须在定义函数时在函数名前加&

2.用引用作函数的返回值的最大的好处是在内存中不产生返回值的副本

//代码来源:RUNOOB
#include<iostream>
using namespace std;
float temp;
float fn1(float r){
    temp=r*r*3.14;
    return temp;
} 
float &fn2(float r){ //&说明返回的是temp的引用,换句话说就是返回temp本身
    temp=r*r*3.14;
    return temp;
}
int main(){
    float a=fn1(5.0); //case 1:返回值
    //float &b=fn1(5.0); //case 2:用函数的返回值作为引用的初始化值 [Error] invalid initialization of non-const reference of type 'float&' from an rvalue of type 'float'
                           //(有些编译器可以成功编译该语句,但会给出一个warning) 
    float c=fn2(5.0);//case 3:返回引用
    float &d=fn2(5.0);//case 4:用函数返回的引用作为新引用的初始化值
    cout<<a<<endl;//78.5
    //cout<<b<<endl;//78.5
    cout<<c<<endl;//78.5
    cout<<d<<endl;//78.5
    return 0;
}

case 1:用返回值方式调用函数(如下图,图片来源:伯乐在线):

img

返回全局变量temp的值时,C++会在内存中创建临时变量并将temp的值拷贝给该临时变量。当返回到主函数main后,赋值语句a=fn1(5.0)会把临时变量的值再拷贝给变量a

case 2:用函数的返回值初始化引用的方式调用函数(如下图,图片来源:伯乐在线)

img

这种情况下,函数fn1()是以值方式返回到,返回时,首先拷贝temp的值给临时变量。返回到主函数后,用临时变量来初始化引用变量b,使得b成为该临时变量到的别名。由于临时变量的作用域短暂(在C++标准中,临时变量或对象的生命周期在一个完整的语句表达式结束后便宣告结束,也就是在语句float &b=fn1(5.0);之后) ,所以b面临无效的危险,很有可能以后的值是个无法确定的值。

如果真的希望用函数的返回值来初始化一个引用,应当先创建一个变量,将函数的返回值赋给这个变量,然后再用该变量来初始化引用:

  int x=fn1(5.0);
  int &b=x;

case 3:用返回引用的方式调用函数(如下图,图片来源:伯乐在线)

float c=fn2(5.0);//case 3:返回引用

img

这种情况下,函数fn2()的返回值不产生副本,而是直接将变量temp返回给主函数,即主函数的赋值语句中的左值是直接从变量temp中拷贝而来(也就是说c只是变量temp的一个拷贝而非别名) ,这样就避免了临时变量的产生。尤其当变量temp是一个用户自定义的类的对象时,这样还避免了调用类中的拷贝构造函数在内存中创建临时对象的过程,提高了程序的时间和空间的使用效率。

case 4:用函数返回的引用作为新引用的初始化值的方式来调用函数(如下图,图片来源:伯乐在线)

float &d=fn2(5.0);

img

这种情况下,函数fn2()的返回值不产生副本,而是直接将变量temp返回给主函数。在主函数中,一个引用声明d用该返回值初始化,也就是说此时d成为变量temp的别名。由于temp是全局变量,所以在d的有效期内temp始终保持有效,故这种做法是安全的。

3.不能返回局部变量的引用。如上面的例子,如果temp是局部变量,那么它会在函数返回后被销毁,此时对temp的引用就会成为“无所指”的引用,程序会进入未知状态。

4.不能返回函数内部通过new分配的内存的引用。虽然不存在局部变量的被动销毁问题,但如果被返回的函数的引用只是作为一个临时变量出现,而没有将其赋值给一个实际的变量,那么就可能造成这个引用所指向的空间(有new分配)无法释放的情况(由于没有具体的变量名,故无法用delete手动释放该内存),从而造成内存泄漏。因此应当避免这种情况的发生

5当返回类成员的引用时,最好是const引用。这样可以避免在无意的情况下破坏该类的成员。

6.可以用函数返回的引用作为赋值表达式中的左值

#include<iostream>
using namespace std;
int value[10];
int error=-1;
int &func(int n){
    if(n>=0&&n<=9)
        return value[n];//返回的引用所绑定的变量一定是全局变量,不能是函数中定义的局部变量 
    else
        return error;
}
 
int main(){
    func(0)=10;
    func(4)=12;
    cout<<value[0]<<endl;
    cout<<value[4]<<endl;
    return 0; 
}

3.运算符重载

·当C++被用于类型的对象时,C++语言允许我们为其指定新的含义;同时我们也能自定义类类型之间的转换规则。

·和其它函数一样,重载的运算符也包含返回类型参数列表、以及函数体

·如果重载的是二元运算符,则其参数有两个,三元的有三个参数,依次类推

·除了(),其他运算符的重载不能有默认实参。

如果运算符函数是成员函数,则其第一个(左侧)运算对象绑定到隐式的this指针上。(因此,成员运算符(显示)参数数量比运算符的运算对象少一个)。

成员函数通过一个名为this的额外隐式参数来访问调用它的对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。如 如果调用total.isbn(),则编译器负责把total的地址传递给isbn的隐式形参this,可以等价地认为编译器将该调用重写成了如下的形式:

Sales_data::isbn(&total).
  • 重载函数调用运算符 ()

如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象

struct absInt()		//接受int类型的对象,然后返回该参数的绝对值。
{
	int operator()(int val) const
	{
		return val<0 ? -val : val;
	}
};

4.内存分区中的堆与栈

  • 栈:栈由操作系统自动释放,用于存放函数的参数值局部变量

    int main() {
    	int b;				//栈
    	char s[] = "abc"; 	//栈
    	char *p2;			//栈
    }
    

    ​ 其中函数中定义的局部变量按照先后定义的顺序依次压入栈中,也就是说相邻变量的地址之间不会存在其它变量。栈的内存地址生长方向与堆相反,由高到底,所以后定义的变量地址低于先定义的变量,比如上面代码中变量 s 的地址小于变量 b 的地址,p2 地址小于 s 的地址。栈中存储的数据的生命周期随着函数的执行完成而结束。

  • 堆:堆由开发人员分配和释放,程序结束时由OS回收

    关于堆上内存空间的分配过程,首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确地释放本内存空间。由于找到的堆节点的大小不一定正好等于申请的大小,系统会自动地将多余的那部分重新放入空闲链表。

  • 堆与栈的区别

    堆与栈实际上是操作系统对进程占用的内存空间的两种管理方式,主要有如下几种区别:
    (1)管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;

    (2)空间大小不同。每个进程拥有的栈大小要远远小于堆大小。理论上,进程可申请的堆大小为虚拟内存大小,进程栈的大小 64bits 的 Windows 默认 1MB,64bits 的 Linux 默认 10MB;

    (3)生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。

    (4)分配方式不同。都是动态分配的,没有静态分配的堆。栈有 2 种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca()函数分配,但是栈的动态分配和堆是不同的,它的动态分配是由操作系统进行释放,无需我们手工实现。

    (5)分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。

    (6)存放内容不同。栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内容(EIP),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(EBP),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者BSS段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。

    从以上可以看到,堆和栈相比,由于大量malloc()/free()或new/delete的使用,容易造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。栈相比于堆,在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆。

    无论是堆还是栈,在内存使用时都要防止非法越界,越界导致的非法内存访问可能会摧毁程序的堆、栈数据,轻则导致程序运行处于不确定状态,获取不到预期结果,重则导致程序异常崩溃,这些都是我们编程时与内存打交道时应该注意的问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值