0
点赞
收藏
分享

微信扫一扫

【C++面向对象高级编程】知识点总结(1)

伊人幽梦 2022-02-20 阅读 71

一些小知识点

关于C和C++

在C语言中,数据和方法(函数)是分开的,用数据来创造变量。

而在C++的类中(struct,class),数据和函数是可以放在一起的(成员数据,成员函数),由这两者共同产生对象。

image-20220115131927710

关于class,也可以看作两大类:带有指针的和不带有指针的。

这两类在实现上有一定的区别,因为不带有指针的可以被class包含在一起。

而带有指针的(比如字符串),会在内存其他地方调用空间,在类中只有指向该空间的指针。

关于头文件

#ifndef ...
#define ...
...
#endif

这样的声明属于防卫式声明,可以防止多个文件中重复的include。

关于内联函数(inline)

内联函数可以解决一些频繁调用的函数大量消耗栈空间(栈内存)的问题。

这里提到一个inline候选人的概念,也就是说是不是真正的inline需要由编译器来决定,函数越简单越有可能。

如果函数在class内定义,则自动成为inline候选人。

如果在class外,可以在前面加上inline声明,但是能不能被选入要看编译器。

inline double imag(const complex& x){
	return x.imag();
}

关于初始化列表

有时的构造函数会以初始化列表的形式出现。

class complex{
public:
	complex (double r=0,double i=0)
	:re(r),im(i){}
private:
    double re,im;
}

也可以使用赋值的方法,比如re=r,但在效率上有差异,这也是使用初始化列表的原因。

这里的例子使用了默认实参,要注意默认实参重载时可能会与无参数发生冲突,编译器无法判断属于哪一类。

构造函数也可以放在private区,也就是不允许外界构造。(是一种设计模式Singleton)

关于const

后面会对const深入探讨,这里先引入一下。我们可以换个角度理解const。在你不想改变值的情况下就加上const,可以在函数后或者变量之前。

 class complex{
 public:    
 	complex (double r=0,double i=0)    :re(r),im(i){}
 	double real() const {return re;}
 	double imag() const {return im;}
 private:
 	double re,im;
 	}

但如果不加,在下面这种情况就会报错

const complex c1(2,1);
cout<<c1,real();
cout<<c1.imag();

编译器会认为这与类的构建产生了冲突。

比较重要的知识点

关于参数传递

参数传递有传值和传指针、引用。

在我们第一次遇见传值和传指针传引用时,应该是遇到swap的时候。

void swap(int a,int b)
{
    int temp = a;
    a = b;
    b = temp;
}
int main(void)
{
    int a = 10;
    int b = 20;
    printf("before swap:a = %d,b = %d\n",a,b);
    swap(a,b);
    printf("after  swap:a = %d,b = %d\n",a,b);
    return 0;

这时候的a与b没有被交换,因为swap函数传入的是值。

传值就相当于创建了原始数据的副本。副本如何操作与原始数据本身没有关系。

而传地址和传指针会改变数据本身,才会实现真正的交换。

(关于引用和指针的关系后面细说,可以把引用看作包装后的指针)

能选择传递引用就不用传递值,引用速度更快(相当于传指针),可以加const让函数不可修改。

ostream&
operator<<(ostream& os,const complex& x){
return os<<'('<<real(x)<<','<<imag(x)<<')';
}

关于this

this可以看作这个class里该对象的地址。

成员函数里会隐含一个this在里面。

inline complex& complex::operator+=(const complex& r){
	return _doapl(this,r);
}

inline complex& _doapl(complex* ths,const complex& r){
   	this->re+=r.re;
    this->im+=r.im;
    return *ths;
}

这时如果执行c2+=c1;c2就相当于this,c1就是r。(注意操作符重载作用于左边)

inline complex& complex::operator+=(this,const complex& r){
	return _doapl(this,r);
}

非成员函数无this,比如

inline complex operator+(const complex& x,const complex& y){
	return ...
}

关于返回值传递

返回值传递也分为传值和传参。

区别在于:

  • 如果返回的值需要一块新的空间来存放,那么就需要传递值,因为函数结束后不保存就会消失。
  • 如果是在已经存在的值上面叠加等操作,不需要新的空间,那么可以回传引用。

根据上面的例子分开来说明这两种情况。

首先是成员函数

inline complex& complex::operator+=(const complex& r){
	return _doapl(this,r);
}

inline complex& _doapl(complex* ths,const complex& r){
   	this->re+=r.re;
    this->im+=r.im;
    return *ths;
}

可以看到这里的操作是在已存在的this上进行的,所以可以返回引用。

传递者无需知道接收者的接收形式,这也是引用的好处

比如_doapl这个函数返回的是*ths,也就是value值本身,但是接收者用引用接收。试想一下如果这里是指针接收那么就需要返回指针,但是引用不需要。

而且返回引用可以实现连续操作。比如c1+=c2+=c3;如果传值那么就会出现上述的不发生改变的情况。

对于非成员函数的例子来说

inline complex operator+(const complex& x,const complex& y){
	return complex(real(x)+real(y),imag(x)+imag(y));
}

注意他们的返回值是由临时对象构建的,也就是说需要新的空间存放,否则会跟着函数消亡。

所以这时返回需要传递值。

关于三大函数

在类中有三个比较特殊的函数:拷贝构造、拷贝赋值、析构

接下来以字符串类为例阐述。

class String
{
public:
	String(const char* cstr=0);//构造函数
	String(const String& str);//拷贝构造
	String& operator=(const String& str);//拷贝赋值
	~String();//析构
	char* get_c_str() const {return m_data;}
private:
	char* m_data;
}

字符串类的特点是它是由指针构成的。而指针指向的空间才真正存放字符串。

对于c++来说,这个字符串结尾为’\0’,占一位。

image-20220116102112972

构造函数

String类的构造函数和析构函数如下,写的很精致。

inline String::String(const char* cstr=0)
{
	if(cstr){
		m_data=new char[strlen(cstr)+1];
		strcpy(m_data,cstr);
	}
	else{
		m_data=new char[1];
		*m_data='\0';
	}
}

inline String::~String()
{
	delete[]m_data;
}

我们根据以下调用来看

String s1();
String s2("hello");

String的构造函数有默认参数为0,如果不为零,则需要分配一块新的空间,大小为该字符串+1(需要结尾的’\0’),然后将字符串拷贝。

如果为0,也就是没有指定初值,那么需要new一块大小为1的空间存放’\0’。

拷贝构造

对于拷贝构造来说,需要给新字符串开辟新空间,再复制原字符串

inline String::String(const String& str)
{
	m_data=new char[strlen(str.m_data)+1];
	strcpy(m_data,str.m_data);
}
String s1("hello");
String s2(s1);

拷贝赋值

拷贝赋值是我们常见的"=“形式,虽然是重载”=",但实际实现也有一些小细节

inline String& String::operator=(const String& str)
{
	if(this==&str)
		return *this;
    
	delete[] m_data;
	m_data=new char[strlen(str.m_data)+1];
	strcpy(m_data,str.m_data);
	return *this;
}

比如执行如下代码时:

String s2=s1;

会首先把s2的原值delete,然后开辟新空间,再将s1的值复制进去。

这里第一步先进行了检测,检测这个要赋的值是不是自己。

这里不是简单地提升效率,如果我们注意到后面的delete就会发现,当等于号左右都是自己的时候,会出现销毁自己的情况,这样就无法完成赋值了。

关于浅拷贝与深拷贝

在我们明白上述几种函数怎么写的时候,浅拷贝和深拷贝的问题就迎刃而解了。

如果我们不写String类的特殊构造函数,让编译器负责处理。

那么在拷贝构造与拷贝赋值时,就会出现直接拷贝的情况,没有new和delete的过程。

因为String是指针类,那么这样就会导致两个指针指向同一块空间,造成内存泄漏。这就是浅拷贝。

image-20220116110748187

深拷贝就是我们这种正确的写法,将每个指针对应于一块内存空间。

所以结论就是带有指针的类必须要有拷贝构造和拷贝赋值,不然会出现浅拷贝现象

举报

相关推荐

面向对象高级编程 - 上

0 条评论