zoukankan      html  css  js  c++  java
  • 《程序员面试宝典》(第三版)笔记整理

    记录一些知识点。 

          不怎样的一本书,具体表现为:1)该详细讲解的地方,或者一笔带过或者讲得不全面或者讲些不相关内容;2)该略过的地方,反而详细起来;3)有一部分错误,如sizeof不计算static变量的大小之类的。虽说如此,收获还是有的——知道了在笔试中常见的知识点。这里的笔记就是对我不熟悉或者理解不全面的知识点去Google和查书而来的。

    http://www.cnblogs.com/FengYan/archive/2012/10/31/2747144.html

    C++的关键字
    1. 使用extern "C"的理由
    函数被C编译器编译后不带参数信息,被C++编译器编译后会带上参数信息。这是因为C++支持函数重载。所以函数被C编译器编译和被C++编译器编译是不同的。例如:void Zero(int lin),被C编译后,可能得到_Zero,被C++编译后,可能得到_Zero_int。那如果要在C++中使用被C编译过的库呢?使用extern "C"就可以!它会告诉C++编译器,被它修饰的函数是以C语言的编译方式编译的。
     
    2. const的作用
    a. 声明常量     
    b. 声明函数形参     -> 提高自定义类型的调用效率。注意,如果是内部类型,如int,没必要写成const int&
    c. 声明函数返回值
    d. 修饰类成员函数     -> 防止成员函数对成员变量进行修改。注意,const成员函数内只能调用类的其他const成员函数
    备注:如果要在const成员函数修改某个成员变量,那么可以给这个变量的声明加上mutable
     
    3. static的作用
    a. 用于函数内部的局部变量时,能保证函数退出作用域后不回收其空间(即其值保持不变)
    b. 用于全局变量时,使变量的作用域限制在一个文件内
    c. 用于函数时,使函数只在一个文件内可见
    d. 用于类成员变量时,代表该变量属于类的(即所有对象共享这个变量)
    e. 用于类成员函数时,代表该函数为整个类所有。注意,该函数不接收this指针,也意味着不能调用一般的成员函数或者变量
     
    4. sizeof操作符
    a. 作用:返回一个对象或者类型所占的内存
    b. sizeof 不计算类/结构体的static成员 -> 因而可将a中提到所占的内存理解为存储某个类型的对象所需要空间
    c. 对空类使用sizeof会返回1     -> 占用内存为0,无法实例化,所以编译器为了使空类也能实例化,就给其分配了1Byte
    d. 如果类中有虚函数,那么最终结果要多加4Byte     -> 此时多出一个指针,指向虚函数表
    e.g:
    static int a;     //sizeof(a) = 4,因为这不是类内/结构体内的
     
    class Zero {
    int a;
    static int b;
    virtual void fun() {}
    };    //sizeof(Zero) = 8
     
    class Lin {
    virtual void fun() {}
    };     //sizeof(Lin) = 4,由d可知其为lin至少有4Byte,所以在这个例子中规则c不成立,因而最终结果为4Byte
     
    class Child: public Zero {
         int a;
         virtual void fun() {}
    };     //sizeof(Child) = 12
     
    5. inline
    a. inline只是一种建议,建议编译器将指定的函数在被调用点展开,因此编译器是可以忽略inline的(即不展开)
    b. inline以代码膨胀(复制)为代价,省去了函数调用的开销,从而提高函数的执行效率
    c. 每一处inline函数的调用都要复制代码,这将使得程序的总代码量增大,消耗更多的内存空间
    d. inline必须与函数定义放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用
    e. 定义在类声明之中的成员函数将自动地成为内联函数
     
    6. explicit
    禁止单参数构造函数被用于隐式类型转换
    class Zero {
    public:
         Zero(int i): lin(i) {}
    private:
         int lin;
    };
    int Fun(Zero z) {}     //如果不用explicit的话,可以这样调用Fun(1);
     
    -------------------------------------------------------------------------------------------
    C++的规则
    1. 在声明赋值语句中,变量先声明,然后赋值
    int i = 1;
    int main() {
         int i = i;     //这里声明的i 覆盖了全局变量的i,之后的赋值就是局部变量的i 给自己赋值,因而其值是未定义的
         return 0;
    }
     
    2. 从右到左压参数
    int main() {
         int i = 1;
         printf("%d, %d ", i, ++i);     //VS2010输出2, 2
    }
    其它:
    虽然压参数的顺序是固定的,但计算顺序是编译器相关的,因此最后的结果与编译器相关。
     
    3. 写宏时要注意
    a. 括号的使用
    b. 不要使用分号
    e.g: 写一个找出两者中较小的宏
    #define MIN(a, b) ( (a) < (b) ? (a) : (b) )
     
    4. 结构体对齐原则
     a. 数据成员对齐规则:结构(struct或union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(e.g: int在32位机为4字节,则要从4的整数倍地址开始存储)
    b. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存(e.g: struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储)
    c. 结构体的总大小,必须是内部最大成员的整数倍,不足的要补齐
    备注:设计结构体,最好把占用空间小的类型排在前面,占用空间大的类型排在后面,这样可以相对节约一些对齐空间
    struct Zero {
    char b;
    int a;     //a的起始位置要按4字节对齐,所以b之后要补3个字节
    short c;
    };     //sizeof(Zero) = 12
    struct OreZ {
    char b;
    short c;     //c的起始位置要按2字节对齐,所以b之后要补1个字节
    int a;
    }     //sizeof(Orez) = 8
     
    5. 结构体位制
    a. 位段成员的类型仅能够为unsigned或者int, 并且指定的位数不能超过类型的长度
    b. 存储规则:如果下一个位段能存放在之前的存储单元中(存储后长度不超过类型长度),则存储,否则,存储到新的存储单元中。因为位段不能跨单元存储
    struct Zero {
         unsigned short a : 4;
         unsigned short b : 5;
         unsigned short c : 7;     //刚好16,符合unsigned short的长度,所以存放在同一单元中
    }zero;
    int main() {
         zero.a = 2;
         zero.b = 3;
         int i = *((short*)&zero);
         printf("%d", i);     //输出50(VS2010测试结果),这说明先声明的变量在低位
         return 0;
    }
     
    6. C++风格类型转换
    a. const_cast<type>(varible): 去掉变量的const或volatile属性
    b. static_cast<type>(varible): 类似C的强制转换,但功能上有所限制(如:不能去掉const属性)     -> 常用于基本类型转换,void指针与目标指针转换
    c. dynamic_cast<type>(varible): 有条件转换,运行时进行类型安全检查(如果失败则返回NULL)     -> 用于基类与子类之间的类型转换(必须要有虚函数)
    d. reinterpret_cast<type>(varible): 仅仅重新解释二进制内容    -> 常用于不同类型的指针转换
    class Base {
    public:
         virtual void fun() {}
    };
    class Zero: public Base {
    public:
         virtual void fun() {}
    };
    int main() {
         Base *pb = new Base;
         (dynamic_cast<Zero*>(pb)) -> fun();     //由于实际指向的是基类,所以此转换失败,返回NULL,导致运行时错误
         delete pb;
         return 0;
    }
     
    7. 指针与引用的差异
    a. 初始化:指针可以不初始化;引用必须在定义时初始化
    b. 非空性:指针可以为NULL,因而指针可能是无效的;引用不能为空,因而引用总是有效的
    c. 内存占用:指针要占用内存,引用不占     -> 引用只是一个逻辑概念,通常在编译时就优化掉了
    d. 可修改性:指针可以重新指向其它变量;引用不可以
    《Effective C++》:在一般情况下,引用和指针是一样的,但是根据条款23:在返回一个对象时,尽量不要用引用,而是用指针
     
    8. 指针与句柄(handle)
    指针标记一个内存的地址
    句柄是Windows用来标识被应用程序建立或使用的对象(窗口,菜单,内存等)的唯一整数。从构造上看,句柄是一个指针,但其不指向存储对象的内存位置,而是一个包含了对象所在的内存位置的结构(结构的复杂度由所指的对象决定)
     
    9. 指针与浅拷贝
    浅拷贝在复制指针时,只是单纯地将指针指向同一内存,并不对所指向的内容进行复制。因而,当成员变量有指针时,最好考虑是否要写深拷贝函数和赋值函数
    class Zero {
    public:
         Zero(): ptr(NULL) {}
         ~Zero() { if( ptr )     delete[] ptr; }
         char* ptr;
    };
     
    int main() {
         Zero z;
         z.ptr = new char[100];
         strcpy(z.ptr, "zero");
         vector<Zero> * vz = new vector<Zero>();
         vz->push_back(z);
         delete vz;
         return 0;
    }   //退出main时会出现运行时错误
    由于Zero没有定义拷贝函数,所以有一个默认的浅拷贝函数。在main函数中,delete vz已经释放过一次ptr指向的内存,然后在离开main的作用域时z的析构函数启动,再次释放该内存
     
    10. 迭代器失效
    在容器中增加/删除元素时,可能会使部分或者全部的迭代器失效
     
    11. C++编译器默认产生的成员函数
    构造函数,析构函数,拷贝函数,赋值函数
     
    12. 类中静态成员变量与常量
    a. 静态成员变量一定要在类外初始化
    b. 成员常量一定要在初始化列表初始化
    c. 静态成员常量一定要初始化,初始化时可以直接在声明时写上,也可以像一般的静态成员变量那样写
    class Zero {
    public:
         Zero():lin2(1) {}
    private:
         static int lin;
         const int lin2;
         static const int lin3 = 1;
    };
    int Zero::lin = 0;
     
    13. 初始化列表的初始化顺序根据成员变量的声明顺序执行
    class Zero {     //不要写这样的代码
    public:
         Zero(int i):lin2(i), lin1(lin2) {}     //此时先初始化lin1,再初始化lin2,因而最后的结果是lin1的值是随机数,lin2的值是i
    private:
         int lin1;
         int lin2;
    };
     
    14. 构造函数不可以为virtual,析造函数有时必须为virtual
    虚函数在运行时动态绑定,动态绑定需要知道动态类型才能进行绑定。而一个类的对象在没有运行构造函数前,其动态类型不完整,因而无法进行动态绑定
    delete指向子类对象的基类指针时,如果析构函数不是virtual,则只会调用基类的析构函数,从而造成内存泄漏
     
    15. C++编译的程序占用的内存
    a. stack(栈区): 由OS自动分配释放空间,主要用来存放函数的参数,局部变量等
    b. heap(堆区): 一般由程序员分配释放空间, 若程序员不释放,程序结束时可能由OS回收
    c. static(全局/静态区): 存放全局变量和静态变量。值得注意的是,初始化和未初始化的这两种变量是分开存放的。
    d. 文字常量区: 存放常量字符串
    e. 程序代码区: 存放函数的二进制代码
     
    -------------------------------------------------------------------------------
    杂项
    1. 不用循环,判断一个数X是否为2的N的次方(N >= 0)
    X & (X - 1)     //最后结果为0,则是2的N次方
    分析:2,4,8的二进制式为10, 100, 1000,当其与1, 3, 7的二进制式相与时,其结果为0
     
    2. 不用判断语句,找出两个数中的最大值
    ((a + b) + abs(a - b)) / 2
     
    3. 不用中间变量,交换两者的值
    a = a ^ b;     //得到a与b的哪些位不相同(即一共有多少位不同)
    b = a ^ b;     //去掉b的不同位,换上a的不同位,从而得到a
    a = a ^ b;
     
    4. 写一个宏FIND,求结构体struc里某个变量相对struc的偏移量
    #define FIND(struc, e) (size_t)&(((struc*)0)->e)
    解析:(struc*)0将0强制转换为struc*的指针类型,然后再取(struc*)0的成员e的地址,从而得到e离结构体首地址的偏移量
     
    5. 数组名再取地址
    int a[] = {1, 2, 3};
    int *ptr = (int*)(&a + 1);     //&a相当于得到二维数组指针,因而&a + 1相当于移动一行
    printf("%d ", *(ptr - 1));    //输出3
     
    6. 指针的移动
    int main() {
         int *pi = NULL;
         pi += 15;
         printf("%d ", pi);     //输出60
         return 0;
    }
     
    7. 整数超范围
    如果是有符号数,那么最大正整数 + 1 等于最小负整数
    如果是无符号数,那么最大正整数 + 1 等于零
    bool IsOdd(int i) {
         return (i & 1) == 1;    //不要写成(i % 2) == 1,因为这样写判断负数时会出问题
    }
    int main() {
         for(int i = 0xFFFFFFFF; i <= 0x7FFFFFFF; ++i) {     //这会陷入死循环,因为最大正整数 + 1 等于最小负数
              cout<<i<<" is "<<IsOdd(i) ? "odd" : "even"<<endl;
         }
         return 0;
    }
     
    8. 基类指针与子类指针
    class Base {
         int a;
    };
    class Zero: public Base {
         int b;
    };
    int main() {
         Zero *pz = new Zero();
         Base *pb = dynamic_cast<Base*>(pz);
         (pz == pb) ? (cout<<"Yes ") : (cout<<"No ");     //VS2010输出"Yes"
         (int(pz) == int(pb)) ? (cout<<"Yes ") : (cout<<"No ");     //VS2010输出"Yes"
         return 0;
    }
     
    9. strcpy的写法
    char* strcpy(char* dst, const char* src) {     //返回char*是为了方便链式调用
         assert( (src != NULL) && (dst != NULL) );     //特别注意检查
         char* tmp = dst;
         while( *dst++ = *src++ );    //++和*的优先级一样,而双者均为右结合,所以*dst++相当于*(dst++)
         return tmp;
    }
     
    10. 概率题
    int main() {
         int cnt = 0;
         for(int i = 0; i < 10000; ++i) {
              int x = rand();
              int y = rand();
              if( x * x + y * y < RAND_MAX * RAND_MAX )     ++cnt;     //实质上是1/4圆与正方形的面积的比较
         }
         printf("%d ", cnt);     //结果约为PI/4 * 10000
    }
     
    11. 以下程序在编译时,哪句会出错
    struct Zero {
         void Lin() {}
    };
    int main() {
         Zero z();     //这样写会被编译器认为是声明一个函数,而不是调用构造函数
         z.lin();     //这句会出错,因为此时的z被认为是未声明的变量
         return 0;
     
    12. 各种排序总结
    算法 稳定性 时间复杂度 空间复杂度 备注
    选择排序 F O(n^2) O(1)  
    插入排序 T O(n^2) O(1) 当序列有序时,时间复杂度为O(n)
    冒泡排序 T O(n^2) O(1) 当序列有序时,时间复杂度为O(n)
    希尔排序 F O(nlogn) O(1) 具体的时间复杂度与所选的增量序列有关
    归并排序 T O(nlogn) O(n)  
    堆排序 F O(nlogn) O(1)  
    快速排序 F O(nlogn) O(logn) 当序列有序时,时间复杂度恶化为O(n^2)
    桶排序 T O(n) O(k)  
  • 相关阅读:
    Javascript-逻辑运算符(&&)
    Javascript-蔬菜运算价格
    Javascript-涨工资案例
    Javascript-数据类型转换
    Javascript-数据类型转换 、 运算符和表达式
    HTML5表单及其验证
    /*使用PHP创建一个数组,保存5個员工的信息(ename/sex/salary/birthday/pic)*/
    CERC2013(C)_Magical GCD
    UVA12546_LCM Pair Sum
    UVA12545_Bits Equalizer
  • 原文地址:https://www.cnblogs.com/johnleo/p/programmer_interview.html
Copyright © 2011-2022 走看看