前言:
找工作需要,最近看了下一些C++的基本概念,为范磊的《零起点学通C++》,以下是一些笔记。
内容:
delete p;只是删除指针p指向内存区,并不是删除指针p,所以p还是可以用的。删除空指针所指向内存是可以的。
堆中的变量和对象时匿名的,没有名称,只能通过指针来访问。
在堆中创建对象时,在分配内存的同时会调用类的构造函数,在删除堆中对象时,会调用类的析构函数。
为了避免内存泄露,在删除一个指针后应该将其其值赋为0。
常量指针是指针指向的内存区域地址不能改变,但是该内存地址里保存的值是可以改变的,比如int a; int * const p = &a;
指向常量的指针表示指针指向的对象是不能被修改的,但是该指针可以被修改,即该指针可以指向另一块目标内存地址。比如const int a = 0; const int *p = &a; 如果A是一个类,也可以为const A* p = new A;
而指向常量的常指针表示指针本身不能被修改,其指向的内存地址内容也不能被修改。比如const int a = 0; const int * const p = &a;
引用就是别名常量。
堆中的地址是用指针来操作的,用不到别名。
如果在main函数前面的其它函数的声明和定义是一起的,则表明这个函数是内联函数!因此当该函数较长时,应该将声明和定义分开。
可以通过指针或引用来返回多个值,因为返回机制只能返回一个值,所以其它的返回值当做参数来传入,通过引用或指针。如果按值传递时,对象很大,则系统开销会很大(比如对象传入和返回,要创建2次临时对象,因此也会多次调用构造函数和析构函数),此时,一般采用按地址传递或按引用传递。
既然引用实现了指针的功能,而且使用起来更加方便,为什么还要指针呢?
这是因为指针可以为空,但是引用不能为空,指针可以被赋值,但是引用只可以被初始化,不可以被赋为另一个对象的别名。如果你想使用一个变量来记录不同对象的地址,那么就必须使用指针,另外指针也可以是多重的。
指针可以指向堆中空间,引用不能指向堆中空间。但指针和引用可以一同使用,另外要注意引用的生命周期。
对于引用而言,如果引用是一个临时变量,那么这个临时变量的生存周期不会小于这个引用的生存期。指针不具备这个特性。
为了避免内存泄露,我们不能按值的方式返回一个堆中对象,而必须按地址或者别名的方式返回一个别名或者内存地址,这样就不会调用复制构造函数来创建一个该对象的副本了,而是直接将该对象的别名或者地址返回。
为了避免指针混淆,我们必须对堆中的内存在哪个函数中创建,就在哪个函数中释放。
如果函数名相同,函数参数的个数也相同,只是参数的类型不同,则也可以是函数重载。
如果函数有缺省值,则在调用时该参数可以不传入,这样相当于函数个数少了。
默认参数其实也可以看做是一种函数重载,但默认参数的函数如果不加标注的话很容易被忽略,而且容易被有参数的同名函数覆盖。而通常的重载函数使用方便,易于理解。具有默认参数的重载的是参数的数值,而重载函数重载的是参数的类型。
我可以对构造函数的函数头对常量和引用进行初始化,此时的初始化顺序是按照成员列表的顺序进行的,而不是按构造函数头赋值的顺序。而析构顺序恰好相反。
一般情况下,编译器会自动为类生成一个默认的复制构造函数。
清楚构造函数和new的结合,析构函数和delete的结合。
构造函数是不能设置为私有的。
默认的构造函数是浅层的构造函数,如果类中的成员变量有指针的话,就很有可能出现内存泄露的错误,因此此时需要使用深层的构造函数。
用已有对象来创建对象时,才会调用复制构造函数。如果复制符左侧是已有对象,右侧也是已有对象,则不会调用复制构造函数,而是调用一个赋值运算符函数。
只要创建一个类,编译器就会自动添加4个函数:构造函数,析构函数,复制构造函数,赋值运算符函数,其中系统默认的复制构造函数和复制运算符都属于浅层拷贝。
默认的自加重载运算符为前置自加运算符。
可以通过创建一个无名的临时对象来完成重载自加后的对象赋值操作。当然了,较好的方法是返回*this指针,这样就不需要建立一个临时对象了,不过它依旧会调用赋值运算符,为了避免这种情况,可以将重载自加函数返回值类型设为返回对象的引用。
重载后置自加运算符时,为了区分与前置自加的区别,需要添加一个毫无意义的参数,这个参数只是在编译器中使用到,在函数体中没用。
在一条语句中自加运算符的执行顺序为从右向左,且不同的编译器入栈的顺序不同,比如说VC6.0中其入栈是要求该语句的表达式执行完后。因此a=1;cout<<a++<<++a;则分别输出的是2,2;
++的结合性为右结合,所以++++i合法,而i++++不合法,可以改为(i++)++;
函数的按值返回时,会建立一个临时的对象,返回完后随后又被销毁。
在符合隐式类型转换的情况下,可以将一个变量赋值给一个对象,但是不能反过来(除非自己重写重载函数)。
重载类型转换运算符是没有返回值的,和构造函数,析构函数一样。
只有派生类对象可以赋值给基类对象,反过来是不行的。另外,基类指针和引用可以指向派生类,反过来也是不行的。
多重继承允许分别设置基类的派生权限。
单一继承中构造与析构的顺序时,先构造的对象后析构。如果是多重继承,则构对象函数的顺序按照继承列表中的顺序来。
多重继承中,如果多个基类中的函数名相同,则在子类对象调用该函数时会产生混淆,此时应该加入类作用域符。
当我们在子类中定义一个与基类相同的同名函数时,那么等于告诉编译器,用子类的函数覆盖掉基类的同名函数,同时将基类的重载函数给隐藏起来了,此时子类对象中不能再调用父类的对应的重载函数了。
const对象只能调用const成员函数。
如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,那么在派生类中访问共同基类中的成员函数时会产生二义性,解决方法是需要指定类的作用域,或者将共同的基类设置为虚基类,因为虚基类不会产生二义性。
一个虚函数被说明为虚函数,在派生类中覆盖了该函数,那么该函数也是个虚函数,不过你也可以在它前面添加关键字virtual,这样更容易理解。
继承所体现出的不同子类相同函数有不同的表现,但是这个并不能体现类的多态性,因为类的多态性要求可以用基类指针来操作子类。
将一个调用函数联接上被正确调用的函数,这一过程叫做函数联编,一般简称为联编,联编分为静态联编和动态联编2种。
需要分清楚在编译时的静态联编,在运行时的静态联编,在编译时的动态联编,在运行时的动态联编这几种情况。
假如我们在虚函数中没有采用指针或引用,那么就无法实现动态联编。
三种调用虚函数的方式,即按值调用,按指针调用和按引用调用,其中只有按值调用不能体现类的多态性,其它2种都可以。
在虚函数中可以使用成员名限定强行解除动态联编。
如果基类中定义了虚函数,析构函数也应该说明为虚函数。这样对内存的回收会更准确些。基类是虚析构函数时,如果派生类被销毁,则首先是调用派生类的析构函数,然后才是基类的析构函数。
数组通常比较大,所以为了节省内存,C++规定数组在程序只有一个原本,由于这个原因,数组在函数中是不可能在创造一个副本的。C++为了追求程序的运行速度,不对数组进行越界检查。
如果a表示的是数组指针,delete[] a,表示删除堆中的指针数组a,而delete a,表示删除堆中的指针数组第一个指针。
用cin输入字符时,会把空格当做是字符串的结束字符,因此可以采用gets()函数。或者使用cin.get()函数,该函数有2个参数,第一个是数组,第二个是字符串的大小。
在重载下标运算符函数时应该注意:1.由于函数的参数即是数组的下标,因此该函数只能带一个参数,不可带多个参数。2.由于下标运算符只限于本类的对象使用,因此不得将下标运算符函数重载为友元函数,且要求是非静态类的成员函数。
由于malloc和free函数产生于C语言时代,因此不可用在c++的对象中,因为对象的产生要调用构造函数。
dynamic_cast的作用是对不同类之间的数据类型进行转换,它可以将一个基类的指针转换成一个派生类的指针。dynamic_cast属于RTTI(运行时信息),需要在工程设置中将其启动。在程序中应该少用RTTI.
如果使用指向基类的指针来指向派生类,那么该指针不能直接调用派生类中多余的成员函数(可以通过强制类型转换),除非该函数在基类中声明成了虚函数。
由抽象类派生的类需要为每一个纯虚函数赋予具体功能。抽象类不能用来定义抽象类对象,但是却可以定义一个指向抽象类的指针。
一个抽象基类仍然可以派生出抽象类,只要该类没有把纯虚函数全部覆盖掉。因为派生出的抽象类,如果其子类没有将它的纯虚函数全部覆盖掉,那么该子类也属于抽象类。
在用单一继承就可以实现的情况下,不要使用多重继承。
静态成员变量需要有定义和声明两个阶段。且定义必须在全局定义,静态成员在没有对象之前已经存在了。
公有的静态成员函数在程序中所有的函数都能够访问它,即包括那些非类的成员函数。而私有静态变量只能被类的成员函数调用,但前提是你必须定义一个对象。
静态成员函数和静态成员变量是一样的,它们不单属于一个对象,而是属于整个类。静态成员函数只能调用静态成员变量,不能调用普通的成员变量。
虽然可以通过对象来访问静态成员函数,但是最好是用类限定符来访问它们。
基类和派生类都可以共享静态成员。
类中任何成员函数都可以访问静态成员,但是静态成员函数不能直接访问非静态成员,只能通过对象名访问该对象的非静态成员。因为静态成员函数是属于整个类的,没有特定指向某个对象的this指针。
静态成员函数不能被说明为虚函数。
函数名是指向函数的第一条指令的常量指针,这与数组名是指向数组中第一个元素的常量指针一样。
函数指针应该将函数名和前面的指针给括起来。
使用函数指针可以减小一些重复的代码,因为函数指针名可以看作是函数名的代号,我们可以通过它来直接调用,函数指针经常会在条件或者判断语句里出现,以便于用户选择调用不同名字但又返回值类型和参数类型完全相同的函数。
函数指针可以作为函数的参数传入。
可以使用typedef来简化函数指针的声明,后面以分号结束,注意这点和宏定义是不同的,也可以在类中使用成员函数指针,或者成员函数指针数组。
空格的ascii码为32,而空字符的ascii码为0.
cout遇到空字符会停止输出,但与cin不同的是,cout可以输出空格,制表符等空字符或者不可见字符。
如果char型数组后面没有添加字符串结束符的话,这它不是一个字符串,而是一个数组。
strlen返回的是字符串结束标志’o’之前的字符串长度,而不是数组长度。sizeof返回的是数组的所占的字节长度。
char字符串的比较只能用循环的方式进行(或者使用库函数strcmp函数),而string的比较可以直接用等号。
不可以直接对char型字符串数组进行赋值操作,而只能使用strcpy函数,或者对数组每个元素一个个的赋值。而string类的字符串可以直接用等号进行赋值。另外,string类中还有一个专门的赋值函数assign,这个函数可指定赋值字符串起点和长度。
查看char型字符串的长度可以用strlen函数。string中用size成员函数实现该功能。当然了string类中的length函数也是一样的功能,两者的区别仅仅在size是为了兼容STL而出现的,而length是早起的string计算长度的版本。
未被初始化的string对象是个空对象,除了字符串结束标志外,没有任何数据。
在char中,strncat可以实现部分字符串的合并。在string中是append函数。同理,char中用strncpy来实现字符串的替换,在string中使用replace来实现该功能,且replace的重载函数中可以兼容char型字符串数组。
char型字符串的拷贝用memmove, string中使用copy成员函数。
string中用insert来插入,其string类是从下标为1开始数的。erase完成字符串的删除。
char型字符串的查找使用函数strchr。而string类中使用find函数实现。该函数有很多变形的函数,比如find_first_not_of函数,表示找到第一个不相同的字符。同理还有find_first_of,find_last_of等函数。反向查找使用rfind()函数,但是它的返回值位置还是从头开始的,并不是从末尾开始的。
string中用compare实现比较,它的c_str()返回一个指向char型的const指针。
char型字符串传参数到函数中时(可以通过指针或者字符串数组传入),可以不用传入字符串的长度,因为它有结束符,可以用来作为其结束的标志。
C++中结构体与类唯一不同的区别是,结构体的成员默认的是公有的,其它类有的,结构体也有,比如继承,多态等。
为了使计算机执行减法,采用了补码的形式,因为引入了负号。
派生类的析构函数会自动调用基类的析构函数。
只要基类中的析构函数被说明为虚函数,那么派生类的析构函数无论说明与否,都自然是虚构函数。
友元函数并不是类的成员函数,而是类的外部函数,但是该外部函数中类的对象可以调用类的私有成员,而一般情况下,类的对象时不能调用类的私有成员的。
常量对象只能调用可操作常量对象的成员函数(即参数括号后面带有const的成员函数),而不是常量对象的成员函数只能被非常量对象调用,因此常量成员函数和非常量成员函数也算是一种函数重载形式。常量对象的私有数据成员是不能够被修改的,除非在构造函数中第一次修改。且const成员函数中不能调用非const函数。
友元类的作用不是相互的,需要单独设置。
包含对象是将另一个类的对象作为该类的成员,而嵌套类是在该类中定义了一种新类型,这个类型只能在该类中使用。由于嵌套类作为一种自定义的数据类型被封装在另一个类中,因此可避免与其它类的名称冲突。
参考资料:
《零起点学通C++》,范磊。