1、c++编程简介
(1)class 的经典分类:c++ class中含有pointer和不含有pointer有很大的区别,可以以此分为两类。
(2)关于面向对象(object-oriented)和基于对象(object-based)
其实这两个概念并没有很明显的界线,不过现在业界比较统一的认为只有完全具有封装、继承、多态三大特点的才能够叫做面向对象,否则即使设计中蕴含了一些对象的概念,也顶多称为基于对象。
2、头文件与类的声明
(1)c中的struct和c++中的class 区别
struct中的 data 都是全局性的 ,data可以被其他函数处理,安全性方面不如c++.
class中将函数和数据 包在一起,对外界函数是不可见的。
(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必须有拷贝构造,拷贝赋值,析构函数。 string s2(s1); 与 string s2=s1;是等同的。 (3)拷贝赋值函数: 写法举例: 8、堆、栈与内存管理 (1)所谓stack(栈)、所谓heap(堆) stack ,是存在于某作用域(scope)的一块内存空间(memory space)。例如当你调用函数,函数本身即会形成一个stack用来放置它所接收的参数,以及返回地址。 在函数本体(function body)内声明的任何变量,其所使用的内存块都取自于上述stack。 Heap,或谓system heap ,是指由操作系统提供的一块global内存空间,程序可动态分配(dynamic allocated)从其中获得若干区块(blocks)。 (2)stack objects 的生命周期 c1便是所谓的stack object ,其生命作用域(scope)结束之际结束这种作用域内的object,又被称为auto object,因为它会被自动清除。 (3)静态对象 c2便是所谓static object,其生命在作用域(scope)结束之后仍然存在,直到整个程序结束。 (4)全局对象 (5)heap objects 的生命周期 p所指的便是heap object,其生命在它被deleted之后结束。 以上出现内存泄漏(memory leak),因为当作用域结束,p所指的heap object 仍然存在,但指针p的生命却结束了,作用域之外再也看不到p(也就没有机会delete p)。 (6)new: 先分配memory,再调用构造函数。 关于string *p=new string(“hello”)的内存过程的个人理解。 :首先new会先分配内存,而我们要明白这个内存里面要装什么,答案是装的是指针。之后调用string类的构造函数,这个构造函数中使用的指针和动态分配内存,即m_data和new。在堆区新建一个内存装的是“hello”,而m_data指向该区。构造函数结束之后,用第一次分配的内存装的是指针m_data,p指向的是m_data这块内存。???不懂! 9、复习string的实现过程 (1)当类中有指针时,要首先考虑到拷贝构造、赋值构造、析构函数; (2)strlen()函数 10、扩展补充:函数模板、类模板及其他 11、组合和继承 (1)复合 构造函数是由内而外,析构函数是由外到内; 12、虚函数与多态 (1)子类继承父类的函数,其实是继承了父类函数的调用权; (2)non-virtual函数:你不希望derived class重新定义(override,复写)它; virtual 函数:你希望derived class重新定义(override,复写)它,而且你对这种函数已经有了默认定义; pure-virtual函数:你希望derived class一定要被重新定义(override,复写),而且你对它没有默认定义; c++
(2)编译器给的拷贝方式是按bit,一个一个比特的拷贝,如上图的浅拷贝,会造成memory leak,变成了孤儿内存。
思考这样一种情况,本来两边的内存都有数据,如果将一边赋值给另一边,必须先将被赋值的一方的内存数据清空,然后调取合适的内存。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;
}
class Complex{
...};
...
{
Complex c1(1,2);
}
class Complex{
...};
...
{
static Complex c2(1,2);
}
class Complex{
...};
...
{
Complex*p=new Complex;
...
delete p;
}
class Complex{
...};
...
{
Complex*p=new Complex;
}
上面是string类的构造函数,我的理解是,string与complex释放内存的差别:由于string内有指针,构造函数中动态分配了内存,而之后定义的时候又一次动态分配了内存,所以才有delete string时析构函数和释放内存的两个操作。char*c="hello";//c本身的长度应该是6,strlen函数取出的是5,是真实长度;
strlen(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,复写),而且你对它没有默认定义;