C++学习笔记(一)

const关键字

1. 一个非const的指针不能指向一个const的变量:
const int d = 1;
int *p = &d; //this is illegal

但是一个const的指针可以指向一个非const的变量。
====================================================
2. 指向const的指针
const指针有两层含义:
(1)const修饰指针指向的变量,即该变量的值不能被修改
(2)const修饰指针,即指针不能再指向别的地方
具体的识别可依据这样的规则:
以指针的*符号为标准,const离谁近就是修饰谁的:
const int* u 修饰int
int const* u 修饰int 
int* const w 修饰指针
====================================================
3.函数的参数和返回值
(1)传递const参数,防止参数在函数里被修改。当参数是地址的时候尤为重要。
(2)返回const值
const int f()
因为函数的返回值是按值拷贝,因此如果返回值是内建类型的时候,完全没有必要使用const限制
但如果是用户定于类型的返回值的时候,用const限定返回的作用是保证函数的返回值不会成为左值(不能再被赋值,也不能被修改)。
(3)类里面的const
const成员变量的含义是:在这个对象生命期内,它是一个常量。然而,对于这个常量而言,不同的对象可以有不同的值。尤其需要注意的一点是,const成员变量只能在初始化列表里赋值!

static成员变量:所有的类对象共享这一个变量

static const成员变量:所有的类对象共享这一个“常量“

const对象: const A a;保证类A的实例a在生命期内的成员变量都不能被修改,因此这样的对象只能调用cosnt成员函数。

const函数(函数右边的const):保证这个成员函数在被调用的时候不会修改成员变量的值,因此可以为const对象所调用。注意,右边加或不加const限定,编译器会认为这是两个函数! 所以,右边的const在定义和声明里都要写。
==================================================
(4)volatile关键字————防止编译器过度优化使得对易变的变量的值读取错误

volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。下面举例说明。在DSP开发中,经常需要等待某个事件的触发,所以经常会写出这样的程序:
short flag;
void test()
{
do1();
while(flag==0);
do2();
}

    这段程序等待内存变量flag的值变为1(怀疑此处是0,有点疑问,)之后才运行do2()。变量flag的值由别的程序更改,这个程序可能是某个硬件中断服务程序。例如:如果某个按钮按下的话,就会对DSP产生中断,在按键中断程序中修改flag为1,这样上面的程序就能够得以继续运行。但是,编译器并不知道flag的值会被别的程序修改,因此在它进行优化的时候,可能会把flag的值先读入某个寄存器,然后等待那个寄存器变为1。如果不幸进行了这样的优化,那么while循环就变成了死循环,因为寄存器的内容不可能被中断服务程序修改。为了让程序每次都读取真正flag变量的值,就需要定义为如下形式:
volatile short flag;

===================================================
===================================================
===================================================

内联函数

1.任何在类中定义的函数都自动的成为内联函数,也可以在非类的函数面前加上inline关键字使之成为内联函数。但为了使之有效,内联函数的定义必须和声明写在一起,即定义写在头文件中,不需要再写在源文件中。
===================================================
2.  inline的原理,是用空间换取时间的做法,是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。所以,如果函数体代码过长或者函数体重有循环语句,if语句或switch语句或递归时,不宜用内联
===================================================
===================================================
===================================================

名字控制
1. static关键字:在函数或类的内部表示静态变量或函数(如果是用户自定义类的对象,则其构造函数随着该函数的调用而调用,其析构函数随着函数的结束而调用),在头文件的全局区表示内部链接(C++默认是内部链接,C默认是外部链接),即该变量只在该文件内部可见,与别的文件联合编译是不可见。
===================================================
2. extern关键字:如果是局部变量或函数则表示在某处已经存在存储区,如果是全局变量则表示外部链接

===================================================
3. 另外两个关键字:
auto:告诉编译器这是一个局部变量,被省略
register:告诉编译器这个变量将会经常用到,应该将它存储在寄存器中以优化速度。
===================================================
4. namespace
namespace的定义如同类的定义一样,可以包含函数,变量,类,如:
namespace Bob = 
{
        class Widget {};
        class Poppit  {};
        extern int i;
        int f()
}
但是不能像一个类一样去创建实例,只能在全局区域定义
namespace 之间可以互相嵌套
也可以赋值:
namespace Bob = namespace BobSuperDuperLibrary;
========================================================
5.namespace的使用
(1). 名字空间中的任何名字都可以用作作用域运算符明确指定,如上例:int i = Bob::f();
(2). 使用using指令可以立即进入整个名字空间,不必在每次引用其中的成员时写完整的域名。
也可以在函数内部引用一个名字空间中的全部内容:
void arithmetic()
{
    using namespace Bob;
    Integer x;
    x.setSign(positive);
}
也可以使用局部成员的声明,如:
using Bob::f();
===================================================
6. 类的静态数据成员的定义要在类的外部,并且在类的定义文件中定义:
class A
{
        static int i;
public:
        .....
}
int A::i = 1;
===================================================
7.嵌套类(类内部的类)的内部可以有静态数据成员,但局部类(函数内部定义的类)的内部不能再定义静态成员
===================================================
8.静态成员函数只能访问静态数据成员,只能调用其他的静态成员函数(没有this指针,不能访问非静态数据或函数)
静态成员函数绝对不能使虚函数(virtual)
===================================================
===================================================
===================================================
引用和拷贝构造函数
1. 当引用被用作函数的参数时,函数内部对这个参数的任何改变将影响函数外部这个变量的值。
int f(int &x){}
int g(int *a){}
...
int main()
{
    int a = 1;
    f(a);  //使用引用会使得对指针的调用变得更隐晦。
    g(&a);
}
==============================================
2. 常量引用
int f(int &){}
int g(const int&){}
...
int main()
{
    f(1); //非法的,不能引用常量
    g(1);
}
另外,编译器在计算期间可能会产生一些临时常量(例如函数返回值的时候),这些常量也被认为是不可以改变的,因此只有常量引用的函数可以以这些临时常量为参数。
==============================================
3.拷贝构造函数
对于用户自定义类型,在没有定义拷贝构造函数的时候:
class A{};
A f(A a);
...
int main()
{
    A a;
    A b = f(a);
}
对于上述的f()的调用会发生:
(1). 参数a按位拷贝在函数内部创建临时对象
(2). 返回值之前建立临时对象,再按位拷贝给b
(3). 调用上述两个临时对象的析构函数

如果用户定义了拷贝构造函数,那么上述的位拷贝就会变成对拷贝构造函数的调用,此时就是按值拷贝而不是按位拷贝。也会产生两个临时对象再销毁
即使是忽略返回值的函数在返回值的时候也会创建临时对象然后再马上销毁。
=============================================
4. 默认拷贝构造函数仅执行位拷贝。
当需要按值拷贝的时候才需要拷贝构造函数,如果不需要,就可以用默认的拷贝构造函数
=============================================
5. 派生类(组合类)的拷贝构造函数调用的顺序是:
先调用基类的拷贝构造函数,再调用组合类成员的拷贝构造函数,对于没有拷贝构造函数的组合类成员,则调用其默认拷贝构造函数。
=============================================
6. 成员数据指针,指向一个对象内部成员的指针
int ObjectClass::*pointerToMemory = &ObjectClass::a;
该指针只能指向ObjectClass内部的任一int类型的成员。
通过该指针获取其指向的内容:
int ObjectClass::*pointerToMemory = &ObjectClass::a;
ObjectClass object1;
object1.*pointerToMemory = 1;
即每个类对象都可以通过调用该指针获取自己内部成员的位置。

类的成员指针仅能给出在类中的具体位置,不能像其他普通指针一样移动或相互比较。
=============================================
7. 成员函数指针
普通函数指针:int (*fp)(float);
定义类的成员函数指针:(可以在创建时初始化,也可以随后赋值初始化)
class Simple2
{
    int f(float) const {return 1;}
};
int (Simple2::*fp2)(float) const;
int main()
{
    fp2 = &Simple2::f;
}
===================================================
===================================================
===================================================
动态对象创建(new,delete,new[],delete[])
1. 局部变量都是在栈里面的,当局部变量出了自己的生命周期后(大括号,函数)后会被编译器自动销毁.但是通过new出来的空间都是在堆上的,需要自己销毁。
===================================================
2. malloc花费时间而且不稳定的原因:
每次malloc都需要搜索内存使用表来找到符合所需大小的空间,这个搜索过程可能极为耗时。
===================================================
3. delete只能用来删除new创建的对象,不能用来删除malloc,calloc,realloc等创建的对象
delete删除空指针的话不发生任何事情。因此建议delete删除一个指针后立即将其赋值为0以防止同一指针被delete两次发生不可预知的后果。
delete删除void*指针将无法调用任何析构函数,这是使用void*指针容易造成内存泄露的原因之一。
一个简单的例子:
class Object
{
    void *data;
public:
    Object()
    {
        data = new char[100];
    }
    ~Object()
    {
        delete []data;
    }
}
...
int main()
{
    Object *a =new Object();
    delete a;
    void *b = new Object();
    delte b;
}
此时delete b只会收回object指针指向的空间,但不会回收内部成员data指针指向的空间,造成内存泄露。
====================================================
4. 一个使指针更像是数组的小技巧:
一般而言,int *q 是等价于int q[];的,但是int *q中的q可以改变指向的位置
因此,写成int* const q可以使得q更像是一个数组。
====================================================
5. new的执行有两个过程:
(1)分配所需要的内存空间;
(2)调用对象的构造函数来初始化这个空间。
delete的执行相反地也有两个过程。

重载new和delete只改变分配内存/回收内存的两个过程,并不影响对构造函数和析构函数的调用的过程。

当在一个类内部重载new或者delete时,只会改变这个类的new和delete的功能(实际就是这个类的一个static函数)。但如果在全局区域重载new或者delete,就会改变所有new和delete的功能。如果都改变,在类内部依然使用自己的重载版本。

operator new() 的返回值是void*类型,代表一块未经初始化的内存空间。参数是size_t(或者其他自己添加的),注意,该参数由编译器自行根据对象类型填充,自己在实际调用new时默认这一位的参数是没有的(重载的new函数可以有多个参数)。
operator delete()的返回值是void,参数是void*,代表经析构函数处理过的空间。
e.g.
void* operator new(size_t sz)
{
    printf("operator new: %d Bytes\n",sz);
    void* m = malloc(sz);
    if(!m) puts("out of memory!");
    return m;
}
void operator delete(void *m)
{
    puts("operator delete");
    free(m);
}
注意这里使用printf()或者puts()而不是用cin,cout等流,原因在于流在初始化的时候系统会调用new来初始化,结果导致死锁循环调用。
=============================================================
6. 要注意new和new[],delete和delete[]都是不同的操作,它们四个可以同时被重载。
其实new和new[]以及delete和delete[]在内存的分配上都已一模一样的,只不过前者申请或释放的内存大小不同,构造函数或析构函数的调用次数也不同。
如果重载new[]操作并观察编译器自动填充的size_t sz参数,就会发现系统实际申请的数组内存大小往往比调用的时候申请的(如new char[100])更多,例如多4,这4个字节使用来数组信息的,特别是数组中对象的数量。
数组operator new() 和 operator delete()只为整个数组调用一次,但对于数组中的每个对象,都调用了构造函数和析构函数。
==============================================================
7. 不要在对象的生存期内随意的调用析构函数来销毁它,因为在对象出了其生命期时系统又会再调用一次它的析构函数,有事会造成严重的后果。
===================================================
===================================================
===================================================
继承和组合
1. 用已有的类创建新的类有两种方式:
(1)新类直接包含已有类的对象实例作为成员变量————组合
(2)用已有的类派生出新的类————继承
在类的构造函数的初始化列表中可以依次调用基类构造函数和成员对象构造函数,例如:
C(int ii):B(ii),a(ii){}
B是基类,a是成员对象
组合与继承的选择:
组合:A has B; 如汽车有轮子
继承:A is B; 如汽车是车
====================================================
2. 在派生类中重载了一个基类的函数,则新类里的基类所有的其他的版本(即使是参数不同的版本)都将被隐藏(无效)。
另外,在派生类中,出了构造函数和析构函数不能被继承以外,operator =()也是不能被继承的。往往在派生类的operator中会调用基类的operator=()函数。
====================================================
3. private继承会使得新类中基类的所有数据和功能是隐藏的,该类的用户访问不到这些内部功能(往往这种情况更好的是使用private组合)
protected继承使得新类无法使用基类的数据和功能,但新类的派生类可以使用,可以看做是面向将来扩展的继承方式。
====================================================
4. 向上造型(upcasting)
如果一个函数需要一个基类的对象作为参数,但实际传参的时候传入的是一个派生类的对象,这个派生类的对象就会被“裁减”(向上造型)成为一个基类对象传入函数。此时这个传入的对象只能调用基类中拥有的函数。即派生类中新加的数据和函数都没有了。
向上造型还能出现在指针和引用的赋值期间:
A -> B
...
B b;
A *pa = &b; //Upcast
A &a = b;  //Upcast
====================================================
5. 在创建派生类的拷贝构造函数时,一定要正确地调用基类的拷贝构造函数(编译器的默认拷贝构造函数就会这样做),否则就会导致派生类中的基类部分缺乏有效的初始化。
===================================================
===================================================
===================================================
多态性和虚函数
1. 向上类型转换会导致的问题是无法知道基类的指针指向的到底是哪一层派生类的实体,当基类和派生类有同名的函数时无法调用正确的版本,virtual函数即多态性能解决这一问题。虚函数的实现是通过晚捆绑(动态绑定,为每个类关联VTABLE)实现的。
dynamic_cast动态向下造型就是通过VTABLE中的信息确定当前派生类的层级的。
===================================================
2. 抽象基类和纯虚函数
只要类中包含一个虚函数(包括虚的析构函数),它就是抽象类
纯虚函数禁止对抽象类的函数以传值方式调用,这也是防止对象切片(object slicing)的一种方法。通过抽象类,可以保证在向上造型期间总是使用指针或引用。
如果派生类希望能产生实例,就一定要定义基类中的所有纯虚函数。
===================================================
3. 派生类的VTABLE准确的把基类的VTABLE中的函数映射到对应的位置,然后把自己新加的虚函数加载基类VTABLE的末尾。
基类指针无法调用到子类中新加的虚函数。只能调用自己已经有的虚函数。
如果明确知道基类指针所指的派生类的类型,可以选择先强制类型转换后在调用派生类中新加的虚函数。
===================================================
4. 对于虚函数而言,子类中重定义的版本(参数列表一致)不能改变其返回值,否则编译器不能确保基类指针能通过多态性调用到预期返回值的函数而出错。但如果参数列表不一致,那就是重写函数,基类中的所有同名的函数版本(无论是不是虚函数)都会被覆盖。
===================================================
5. 虚函数的返回类型
如果基类的一个虚函数返回该类的指针或引用,那么派生类中的该函数允许改变返回值类型为相应的派生类的指针或引用,否则就会发生向上造型丢失信息。
=====================================================
6. 构造函数一定不能是虚函数,但析构函数可以是(最好就是)虚函数。
=====================================================
7. 纯虚函数没有函数体,但纯虚析构函数一定要有一个函数体(编译器一定会产生一个默认析构函数)。此时即使派生类中没有定义析构函数也不会成为抽象类。
这样理解的话纯虚析构函数的唯一作用就是阻止基类实例化。
====================================================
8. 在构造函数和析构函数内部,虚机制无效。即如果在这两种函数调用虚函数,只会调用函数的本地版本。这样做是有数据安全性和可靠性的考虑的。
====================================================
9. virtual运算符重载,多重指派
考虑重载乘法运算符在继承时会发生的事情:
A -> B
A -> C
在A中:
virtual const A& operator*(cosnt A& right){}
B,C中都有对A中运算符*的重载版本
如果写A &a1 = (B或C的对象),A &a2 = (B或C的对象),再运算a1*a2会发生什么事情?
因为*的两边都需要判断指针实际指向的对象的类型,一个虚函数只能进行单一指派————即判断一个未知对象的类型。
此时需要多重指派的技术来解决同时判断两个对象的类型的问题(略)
===================================================
===================================================
===================================================







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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值