C++学习(46)

1.      在声明类时,关键字private/public/protected出现任意次数。(正确)

 

2. 最大公约数的最常用的算法是欧几里得算法,也称为辗转相除法.

问题定义为求i和j的最大公约数gcd(i,j),其中i和j是整数,不妨设i>j.

 

算法可以递归的表示:

1).如果j能整除i,那么gcd(i,j)=j;

2).j不能整除i,令r=i%j,那么gcd(i,j)=gcd(j,r).

使用C语言实现:

 

int gcd(int i, intj)

{int r = i % j;

return r == 0 ? j: gcd(j, r);

}

正确性分析:

算法的步骤1,显然成立(最大公约数定义).关键是要证明步骤2.

设d是i和j的最大公约数,

那么i=md,j=nd,m和n互质(否则d不是最大公约数).

由r=i%j可以得到i=kj+r,k=⌊m/n⌋,k≥1(我们前面假设过i>j).

把i=md,j=nd代入得到

md=knd+r

那么

r=(m-kn)d

m-kn和m也是互质的.

所以得到d是j和r的最大公约数.

 

时间复杂度分析:

逆着看该算法,最后的余数是0,倒数第二次余数是d,倒数第三次是kd,k>1…

由于组成了一个数列,{0,d,kd,nkd+d,…}

数列的n项加上n+1项,比n+2项要小,所以比斐波纳契数列增长的要快.

我们已知斐波纳契数列增长速度是指数,那么待分析的数列也是指数增长.

设欧几里得算法需要k次,那么j=O(2^k),则k=O(lg j).

 

所以欧几里得算法求最大公约数的时间复杂度是对数量级的,速度非常快.

 

3.栈VS.堆

:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

 

堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

 

在C/C++中,内存一般分为,堆区,栈区,全局区,文字长量区,程序代码区!在函数中定义的局部变量是存在在栈区(除static局部变量,它是存在在全局区),动态生成的变量存在在堆区,由指针进行读写!全局变量,静态全局变量,静态局部变量是存放在全局区的! 堆是程序员进行申请和释放的,因此堆是向上,也就是向高地址方向的!栈是系统进行释放的,且栈区大小一般是定的2M,因此栈是向下,也就是向低地址方向! 另外说下,静态局部变量,静态全局变量和全局变量的区别,静态变量没有初始化时,系统会给默认值,而全局变量不会!全局变量在整个工程中都是可见的,而静态全局变量只在本文件中可见,静态局部变量只在此函数内部可见,但函数结束后不释放!

 

对于堆,大量的new/delete操作会造成内存空间的不连续。

堆容易产生memory leak。

堆的效率比栈要低的多。

栈变量引用容易造成逃逸。

栈生长方向是向下的,即向着内存地址降低的方向。

栈区一般由编译器自动分配释放,堆区一般由程序员分配释放。

 

4.运行下列程序的结果:未定义行为。

#include<iostream>
#include<cstdlib>
#include<string.h>
using namespacestd;
void test(void) {
    char *str=(char *)malloc(100);
    strcpy(str,"hello");
    free(str);
    if(str!=NULL) {
        strcpy(str,"world");
        printf(str);
    }
}
int main(intargc,char *argv) {
    test();
    return 0;
}

分析:指针释放存储空间后没有置为NULL,变成野指针。

野指针,不能通过简单的NULL进行判断,delete或free只是释放了指针所指向的内存区域,并没有干掉指针本身,所以指针指向的是“垃圾”指针,所以free或者delete之后要把指针置为NULL。

 

5.分析下列程序:

#include<iostream>
#include<cstdlib>
#include<string.h>
using namespacestd;
class Myclass {
    public:
        Myclass(int i=0) {
            cout<<i;
        }
        Myclass(const Myclass &x) {
            cout<<2;
        }
        Myclass &operator=(const Myclass&x) {
            cout<<3;
            return *this;
        }
        ~Myclass() {
            cout<<4;
        }
};
 
int main(intargc,char *argv) {
    Myclass obj1(1),obj2(2);
    Myclass obj3=obj1;
    //obj3=obj1;
    return 0;
}

分析:拷贝构造函数发生在对象还没有创建,需要创建时,如obj3;赋值操作符重载仅发生在对象已经执行过构造函数,即已经创建的情况下。

 

首先程序中存在三个MyClass对象。前两个对象构造时分别输出1,2;第三个对象是这样构造的MyClass obj3 = obj1;这里会调用拷贝构造函数,输出2;然后三个对象依次析构,输出444;所以最终输出122444。

 

MyClass obj3 =obj1; obj3还不存在,所以调用拷贝构造函数输出2,如果obj3存在,obj3=obj,则调用复制运算符重载函数,输出03。

 

6.分析下列程序

#include<iostream>
#include<cstdlib>
#include<string.h>
using namespacestd;
class A {
    public:
        virtual void func(int val=1) {
            cout<<"A->"<<val<<endl;
        }
        virtual void test () {
            func();
        }
};
class B :public A{
    public:
        void func(int val=0) {
            cout<<"B->"<<val<<endl;
        }
};
 
int main(intargc,char *argv) {
    B *p =new B;
    p->test();
    return 0;
}

分析:缺省参数是静态绑定的,对于这个特性,估计没有人会喜欢。所以,永远记住:

“绝不重新定义继承而来的缺省参数(Never redefine function’s inherited default parameters v)

 

记住:virtual 函数是动态绑定,而缺省参数值却是静态绑定。意思是你可能会在“调用一个定义于派生类内的virtual函数”的同时,却使用基类为它所指定的缺省参数值。

 

结论:绝不重新定义继承而来的缺省参数值!(可参考《Effective C++》条款37)

 

对于本例:

B*p =newB;p->test();

p->test()执行过程理解:

 

(1)由于B类中没有覆盖(重写)基类中的虚函数test(),因此会调用基类A中的test();

(2)A中test()函数中继续调用虚函数 fun(),因为虚函数执行动态绑定,p此时的动态类型(即目前所指对象的类型)为B*,因此此时调用虚函数fun()时,执行的是B类中的fun();所以先输出“B->”;

(3)缺省参数值是静态绑定,即此时val的值使用的是基类A中的缺省参数值,其值在编译阶段已经绑定,值为1,所以输出“1”;

 

最终输出“B->1”。所以大家还是记住上述结论:绝不重新定义继承而来的缺省参数值

 

补充:若是在B类中补充一个test()函数,便可以实现输出B->0。

 

改动(1)

#include<iostream>
#include<cstdlib>
#include<string.h>
using namespacestd;
class A {
    public:
        virtual void func(int val=1) {
            cout<<"A->"<<val<<endl;
        }
        virtual void test () {
            func();
        }
};
class B :public A{
    public:
        void func(int val=0) {
            cout<<"B->"<<val<<endl;
        }
        void test() {
            func();
        }
};
 
int main(intargc,char *argv) {
    A *p =new B;
    p->test();
    return 0;
}
输出结果是:B->0

 

改动(2)

#include<iostream>
#include<cstdlib>
#include<string.h>
using namespacestd;
class A {
    public:
        virtual void func(int val=1) {
            cout<<"A->"<<val<<endl;
        }
        virtual void test () {
            func();
        }
};
class B :public A{
    public:
        void func(int val=0) {
            cout<<"B->"<<val<<endl;
        }
        void test() {
            func();
        }
       
};
 
int main(intargc,char *argv) {
    A *pa =new B;
    B *p =new B;
    pa->test();
    pa->func();
    p->test();
    p->func();
    return 0;
}
 


8.一个类A,其数据成员如下:

class A {
    private :
        int a;
    public :
         const int b;
         float * &c;
         static const char * d;
         static double * e;
};

则构造函数中,成员变量一定要通过初始化列表来初始化的是:bc

分析:构造函数初始化时必须采用初始化列表一共有三种情况,

1).需要初始化的数据成员是对象(继承时调用基类构造函数)

2).需要初始化const修饰的类成员

3).需要初始化引用成员数据

 

因为static属于类并不属于具体的对象,所以 static成员是不允许在类内初始化的,那么static const 成员是不是在初始化列表中呢?答案是NO.

一是static属于类,它在未实例化的时候就已经存在了,而构造函数的初始化列表,只有在实例化的时候才执行。

二是static成员不属于对象。我们在调用构造函数自然是创建对象,一个跟对象没直接关系的成员要它做什么呢.

 

补充说明:

1),const定义的常量在超出其作用域之后其空间会被释放,而static定义的静态常量在函数执行后不会释放其存储空间。

2),static表示的是静态的。类的静态成员函数、静态成员变量是和类相关的,而不是和类的具体对象相关的。即使没有具体对象,也能调用类的静态成员函数和成员变量。一般类的静态函数几乎就是一个全局函数,只不过它的作用域限于包含它的文件中。

3),C++中,static静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化,如:double Account::Rate = 2.25;static关键字只能用于类定义体内部的声明中,定义时不能标示为static.

4),在C++中,const成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数

 

const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类的声明中初始化const数据成员,因为类的对象没被创建时,编译器不知道const数据成员的值是什么。

 

const数据成员的初始化只能在类的构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者static cosnt.

9.protected不可在类外访问。

 

10. 编译器总是根据类型来调用类成员函数。但是一个派生类的指针可以安全地转化为一个基类的指针。这样删除一个基类的指针的时候,C++不管这个指针指向一个基类对象还是一个派生类的对象,调用的都是基类的析构函数而不是派生类的。如果你依赖于派生类的析构函数的代码来释放资源,而没有重载析构函数,那么会有资源泄漏。所以建议的方式是将析构函数声明为虚函数。

 

也就是delete a的时候,也会执行派生类的析构函数。

 

一个函数一旦声明为虚函数,那么不管你是否加上virtual 修饰符,它在所有派生类中都成为虚函数。但是由于理解明确起见,建议的方式还是加上virtual 修饰符。

 

构造方法用来初始化类的对象,与父类的其它成员不同,它不能被子类继承子类可以继承父类所有的成员变量和成员方法,但不继承父类的构造方法)。因此,在创建子类对象时,为了初始化从父类继承来的数据成员,系统需要调用其父类的构造方法。

 

如果没有显式的构造函数,编译器会给一个默认的构造函数,并且该默认的构造函数仅仅在没有显式地声明构造函数情况下创建。

 

构造原则如下:

1.)如果子类没有定义构造方法,则调用父类的无参数的构造方法。

2.)如果子类定义了构造方法,不论是无参数还是带参数,在创建子类的对象的时候,首先执行父类无参数的构造方法,然后执行自己的构造方法

3.)在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数,则会调用父类的默认无参构造函数。

4.)在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数且父类自己提供了无参构造函数,则会调用父类自己的无参构造函数。

5.)在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数且父类只定义了自己的有参构造函数,则会出错(如果父类只有有参数的构造方法,则子类必须显示调用此带参构造方法)。

6.)如果子类调用父类带参数的构造方法,需要用初始化父类成员对象的方式,

 

 

构造函数是不能被继承的,但是可以被调用,如果父类重新定义了构造函数,也就是没有了默认的构造函数,子类创建自己的构造函数的时候必须显式的调用父类的构造函数。

 

缺省构造函数,拷贝构造函数,拷贝赋值函数,以及析构函数这四种成员函数被称作特殊的成员函数。这4种成员函数不能被继承。

 

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值