c++面向对象高级开发_侯捷_笔记

1、c++编程简介

(1)class 的经典分类:c++ class中含有pointer和不含有pointer有很大的区别,可以以此分为两类。

(2)关于面向对象(object-oriented)和基于对象(object-based)

其实这两个概念并没有很明显的界线,不过现在业界比较统一的认为只有完全具有封装、继承、多态三大特点的才能够叫做面向对象,否则即使设计中蕴含了一些对象的概念,也顶多称为基于对象。

c++面向对象高级开发_侯捷_笔记_第1张图片

2、头文件与类的声明

(1)c中的struct和c++中的class 区别

struct中的 data 都是全局性的 ,data可以被其他函数处理,安全性方面不如c++.

class中将函数和数据 包在一起,对外界函数是不可见的。

(2)头文件里面的防卫式声明

c++面向对象高级开发_侯捷_笔记_第2张图片
防卫式声明的意义:#ifndef_COMPLEX_ 进入这个头文件程序时,如果没有定义,就开始定义为所示名称。

这样在程序中重复include时,就不会进入此头文件程序的里面。

防卫式声明的作用是:防止由于同一个头文件被包含多次,而导致了重复定义。

#ifndef 依赖于宏定义名,当宏已经定义时,#endif之前的代码就会被忽略,但是这里需要注意宏命名重名的问题;

#pragma once 只能保证同一个文件不会被编译多次,但是当两个不同的文件内容相同时,仍然会出错。而且这是微软提供的编译器命令,当代码需要跨平台时,需要使用宏定义方式。

3、构造函数

(1)有的函数可以在函数声明之内定义,有的则可以在类外定义。在里面定义就是 inline 函数。

其实最后到底能不能成为真正的 inline 函数是由编译器决定的。例如比较复杂的函数,就算声明了 inline 或者在class内定义,也不不一定是真正的 inline 函数。关键字 inline 的作用其实是一个建议,最后由编译器决定。

(2)关于构造函数的两种方法。

第一种:

complex(double r=0,double i=0)
: re(r), im(i)   //初值列  初始列
{
     }

第二种:

complex(double r=0,double i=0)
{
     
re=r;			//赋值
im=i
}

这两种方法,首选第一种(因为正规),具体原因是效率问题。

简单来说,构造函数数值的设定有两个阶段,第一个是初始化,第二个是赋值,所以说第二种方法的赋值舍去了效率更高的初始化,所以效率较前者比较低。

(3)前面提到了用是否包含 pointer 将class分为 两类。在没有pointer的class中 多半不用写析构函数。

(4)关于函数重载,不能按返回值类型重载。函数重载时虽然程序中函数名称相同,但是在函数编译时,会产生的名称是不同的。

(5)下面两个构造函数是不能同时存在的,例如当 complex c1 是两个构造函数都满足条件,就会产生冲突。

complex(double r=0,double i=0)
: re(r), im(i)   //初值列  初始列
{
     }

complex():re(0),im(0)
{
     }

(6)构造函数也有可能放入private里面。有一种设计模式叫 Singleton ,就是采用了这种方法。

4、参数传递和返回值

(1)class 中的函数可以按照是否改变数据内容而分类。不改变数据内容的函数要果断在()后{}前加上const。

(2)关于const,很重要的一点。例如如果是创建了一个const对象,当这个对象调用不用const修饰的成员函数时,会发生错误。例如:

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


const complex c1(2,1);
cout<<c1.real();//如果在class中real()函数没有被定义成const,那么就会报错;

(3)参数传递尽量(如果可以)传引用,引用的底层还是指针。这样效率会比较高。(另外一种情况,因为一个指针在32位系统中占4个字节,而一个字符char 是占一个字节,这种情况下,传值的效率会更高)。如果传引用不希望函数修改本体,可以加const, 例如形参里面是const int&.

(4)返回值传递也尽量(如果可以)传引用。

(5)相同的 class 的各个 objects 互为 friends(友元)

例如

class complex{
      
public:
    complex(double r=0,double i=0)
        :re(r),im(i)
        {
      }
    int func(const complex& param)
    {
      return param.re+param.im;}    //相同的 class 的各个 objects 互为 friends(友元)
    ...							// 可以直接拿data
    
};
c2.func(c1);

(6)当一个函数返回的是局部变量或者临时变量时,不要使用引用传递。

string &mainp(const  string &s)
{
     
	string ret = s;
	return ret;
}//在之后的main函数中调用就是错误的

(7) 临时变量不能作为非const引用参数,不是因为他是常量,而是因为c++编译器的一个关于语义的限制。如果一个参数是以非const引用传入,c++编译器就有理由认为程序员会在函数中修改这个值,并且这个被修改的引用在函数返回后要发挥作用。但如果你把一个临时变量当作非const引用参数传进来,由于临时变量的特殊性,程序员并不能操作临时变量,而且临时变量随时可能被释放掉,所以,一般说来,修改一个临时变量是毫无意义的,据此,c++编译器加入了临时变量不能作为非const引用的这个语义限制,意在限制这个非常规用法的潜在错误。

#include 
using namespace std;

void f(int &a)

 {
     
 cout << "f(" << a  << ") is being called" << endl;
}

void g(const int &a)

{
     
 cout << "g(" << a << ") is being called" << endl;
}

 int main()

 {
     
 int a = 3, b = 4;
 f(a + b);  //编译错误,把临时变量作为非const的引用参数传递了
 g(a + b);  //OK,把临时变量作为const&传递是允许的
}

5、操作符重载和临时变量

(1)操作符重载-1 成员函数

c1+=c2 //操作符先作用在其左边的数身上,然后在调用其成员函数。
inline complex& _doapl(complex*ths,complex& r){
       //相加函数的实现
...
return *this;
}
inline complex& operator+=(complex& r){
     
    return _doapl(this,r);
    
}

思考一个问题,上述的第二个返回类型,即 complex& 能不能换成 void?

如果我们这样,c3+=c2+=c1;

流程是先计算c2+=c1;

如果换成 void 那么进行第二个 +=之前,返回的是void ,那么就无法满足形参类型为complex 了 ,所以不能换。

(2)操作符重载-2,全局函数

inline complex operator + (const complex&a, double b){
     
	return complex(a.real() + b, a.vich());
}

显然,这个函数的返回不能传引用,因为里面创建的是一个临时对象。函数结束之后,本体就消失了。

(3)临时对象(temp object)

创建方式 :typename();

(4)特殊的重载 “ << ”

操作符重载时,只可能作用在其左边。当重载“ << ”符号时,(以负数类为例),显然不能使用成员函数的方法,只能是全局函数。

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

cout其实是一个对象,它的类型属于ostream, 思考一个问题:ostream& os 前能不能加const。 答案是不能,因为如果加了const 就代表在函数内部不能改变 os ,而函数中 << 就代表往os里面丢东西,使其在屏幕上显示,其实也就代表了改变了os(cout)。

再思考一个问题:函数的返回值能不能是void ,能不能是const?

答案是不能,例如cout<

6、复习complex类的实现过程

7、三大函数:拷贝构造,拷贝赋值,析构

(1)如果你所写的class中带有指针,那么就不要选择编译器给的拷贝函数,需要自己写。class with pointer members必须有拷贝构造,拷贝赋值,析构函数。

c++面向对象高级开发_侯捷_笔记_第3张图片
(2)编译器给的拷贝方式是按bit,一个一个比特的拷贝,如上图的浅拷贝,会造成memory leak,变成了孤儿内存。

string s2(s1); 与 string s2=s1;是等同的。

(3)拷贝赋值函数:
思考这样一种情况,本来两边的内存都有数据,如果将一边赋值给另一边,必须先将被赋值的一方的内存数据清空,然后调取合适的内存。

写法举例:

inline string& string::operator= (const string&str){
     
    if(this==&str){
         //判断是否是自我赋值 这个是必须的,因为思考一下,如果s1和s2都是指向同
        return *this;	//一个内存,那么直接释放s2的话,那么s1也被释放,这样就都没了。
    }
     delete[] m_data;
     m_data=new char[strlen(str.m_data)+1];
     strcpy(m_data,str.m_data);
     return *this; 
}

8、堆、栈与内存管理

(1)所谓stack(栈)、所谓heap(堆)

stack ,是存在于某作用域(scope)的一块内存空间(memory space)。例如当你调用函数,函数本身即会形成一个stack用来放置它所接收的参数,以及返回地址。

在函数本体(function body)内声明的任何变量,其所使用的内存块都取自于上述stack。

Heap,或谓system heap ,是指由操作系统提供的一块global内存空间,程序可动态分配(dynamic allocated)从其中获得若干区块(blocks)。

(2)stack objects 的生命周期

class Complex{
     ...};
...
{
     
    Complex c1(1,2);
}

c1便是所谓的stack object ,其生命作用域(scope)结束之际结束这种作用域内的object,又被称为auto object,因为它会被自动清除。

(3)静态对象

class Complex{
     ...};
...
{
     
    static Complex c2(1,2);
}

c2便是所谓static object,其生命在作用域(scope)结束之后仍然存在,直到整个程序结束。

(4)全局对象

(5)heap objects 的生命周期

class Complex{
     ...};
...
{
     
    Complex*p=new Complex;
    ...
        delete p;
}

p所指的便是heap object,其生命在它被deleted之后结束。

class Complex{
     ...};
...
{
     
    Complex*p=new Complex;
}

以上出现内存泄漏(memory leak),因为当作用域结束,p所指的heap object 仍然存在,但指针p的生命却结束了,作用域之外再也看不到p(也就没有机会delete p)。

(6)new: 先分配memory,再调用构造函数。

c++面向对象高级开发_侯捷_笔记_第4张图片

c++面向对象高级开发_侯捷_笔记_第5张图片
1,清除数据,2,释放内存。

c++面向对象高级开发_侯捷_笔记_第6张图片
上面是string类的构造函数,我的理解是,string与complex释放内存的差别:由于string内有指针,构造函数中动态分配了内存,而之后定义的时候又一次动态分配了内存,所以才有delete string时析构函数和释放内存的两个操作。

c++面向对象高级开发_侯捷_笔记_第7张图片

关于string *p=new string(“hello”)的内存过程的个人理解。

:首先new会先分配内存,而我们要明白这个内存里面要装什么,答案是装的是指针。之后调用string类的构造函数,这个构造函数中使用的指针和动态分配内存,即m_data和new。在堆区新建一个内存装的是“hello”,而m_data指向该区。构造函数结束之后,用第一次分配的内存装的是指针m_data,p指向的是m_data这块内存。???不懂!

9、复习string的实现过程

(1)当类中有指针时,要首先考虑到拷贝构造、赋值构造、析构函数;

(2)strlen()函数

char*c="hello";//c本身的长度应该是6,strlen函数取出的是5,是真实长度;
strlen(c);

10、扩展补充:函数模板、类模板及其他

11、组合和继承

(1)复合

构造函数是由内而外,析构函数是由外到内;

12、虚函数与多态

(1)子类继承父类的函数,其实是继承了父类函数的调用权;

(2)non-virtual函数:你不希望derived class重新定义(override,复写)它;

​ virtual 函数:你希望derived class重新定义(override,复写)它,而且你对这种函数已经有了默认定义;

​ pure-virtual函数:你希望derived class一定要被重新定义(override,复写),而且你对它没有默认定义;

c++
char*c=“hello”;//c本身的长度应该是6,strlen函数取出的是5,是真实长度;
strlen©;




10、**扩展补充:函数模板、类模板及其他**



11、**组合和继承**

(1)复合

构造函数是由内而外,析构函数是由外到内;

12、**虚函数与多态**

(1)子类继承父类的函数,其实是继承了父类函数的调用权;

(2)non-virtual函数:你不希望derived class重新定义(override,复写)它;

​		virtual 函数:你希望derived class重新定义(override,复写)它,而且你对这种函数已经有了默认定义;

​		pure-virtual函数:你希望derived class一定要被重新定义(override,复写),而且你对它没有默认定义;

你可能感兴趣的:(c++面向对象高级开发_侯捷_笔记)