C++笔记

来源:互联网 发布:四川大学软件学院考研 编辑:程序博客网 时间:2024/06/16 18:32

1.静态变量staticthis!

(1)非静态方法参数传递时,有一个隐式参数this,这个this就是调用用该方法的对象本身。

比如,Object o=new object();   o.toString(Object this),o==this;

这样在非静态方法中就可以通过this来得到调用对象的其他域和方法!

(2)静态方法(函数)是属于类的,而不属于某个对象,所以没有隐式形参this,自然不能通过this来调用对象本身!

(3)静态函数只能调用静态的成员和函数,非静态函数无论是静态还是非静态都能调用!

(4)静态变量调用.  Class A{…  static print()…}  调用A.print()或者 A.a  a.print();

(5)静态变量在类中只有一份拷贝 A a,b; 改变A中的静态变量值  会影响中静态变量值!

(6)this只能在类中使用! 作用是区分类中的变量的相同名字和返回*this!

 

2.返回局部变量的引用或指针!(可见第31点,分析更透彻)

例子

#include<iostream>

using namespace std;

 

class A

{

public:

         A()

         {

                   a1=3;

                   a2=4;

         }

         A(int a,int b)

         {

        a1=a;

                   a2=b;

         }

 

         A(const A& m)

         {

                   a1=m.a1;

                   a2=m.a2;

                   cout<<"kaobei hanshu"<<endl;

         }

         ~A()

         {

                   cout<<"xigou"<<endl;

 

         }

(*a)  operator+(const A& r)

         {

                    A t;

                    t.a1=a1+r.a1;//去掉 A t; a1=a1+r.a1;  return (*this)

                    t.a2=a2+r.a2;

return  t;

         }

         A operator=(const A& t)

         {

                   a1=t.a1;

                   return (*this);

         }

         int a1;

         int a2;

};

void main()

{

         A  b(100,101),c(1000,1000);

A  a=b+c;

a=b=c;

cout<<a.a1<<endl;

         cout<<a.a1<<endl;

         cout<<a.a1<<endl;

}

(1)     当(*a)处返回的是对象的时候,执行到return  t 时候会调用复制构造函数,将t的一个副本(因为是值返回,所以避免函数外部修改了函数内部变量的值,于是产生了一个副本,当使用完副本后,会调用析构函数将它释放) 传递到复制构造函数的m(因为m为引用,所以mt的副本别名而已),然后把m(即t的副本)的a1a2赋值到当前的aa1,a2(此时又要调用复制构造函数);所以输出a1,a2是所想要的值!

(2)     当形参和返回值是引用的时候,不会调用拷贝函数,引用值是被引用对象的一个别名!

当形参和返回值是对象的时候,此时会调用拷贝函数,产生一个实体的副本,将副本进行操作,修改,从而不影响到实体!副本使用完后,会自动调用析构函数,将其释放掉!在(1)中已经进行说明!

(3)     A  a=b+c  此时调用一次拷贝函数,调用拷贝函数是在函数operator+ 返回值时调用的,调用后就将aa1的值修改了!

(4)     如果直接调用拷贝函数,而不是函数返回和传参的时候,就不会产生中间变量,所以也不能调用析构函数!直接调用:上面的(2)(3 例子!

(5)     A  a=b+c, operator +函数的返回类型改为引用(即A & operator)或者为(A operator),此时两种情况都是总共会调用4次析构函数,1此拷贝构造函数(其中3此时析构a,b,c)(因为此时编译器做了优化!

(6)     6A  a; a=b+c;  保持上面的例子,operator函数的返回类型改为引用,此时会调用2次拷贝,2次析构函数! ,最后还要调用3次析构,析构掉a,b,c;

注意:这是返回的是局部变量的引用,一定会出问题,这里只是为了举例才这样做!)

(7)     A  a; a=b+c;  operator函数的返回类型改为对象,此时会调2次拷贝构造函数,6次析构函数;(2次拷贝函数:1次是在实现operator +函数返回的时候有1次,第二次是在oprator =函数返回的时候;6次析构:operator +operator =返回的时候分别一次,operator +中的t变量要析构,最后三次是a,b,c对象)

(8)     不要返回局部变量的引用,因为局部变量在函数调用完后会自动释放掉(在栈中),所以返回局部变量的引用的接受者在使用的时候,将毫无用处!

(9)     尽量使用引用,因为引用会避免拷贝函数和析构函数的使用,减少时间,优化程序

(10) 当使用引用的时候要时刻想到该引用是谁的别名,既是它指向了谁!

(11) 执行a=b=c 的时候,因为是又结合性,所以先执行b=c,执行完后会返回值,并产生一个零时变量,使用临时变量又给a赋值,赋值完后又会产生一个零时变量,但是没有使用

(12) 如果只是a=boperator = 返回类型不必是A,返回void 也能实现赋值!原因:因为它并没有使用返回的对象,如果是a=b=c 就不能返void

(13) 执行A  a=b+c 时候,此时会调用 operator+ 其中 b 就是函数内部的(*this)指针,所以执行a1=a1+r.a1 即修改了ba1 注意!

 

 

3.赋值与初始化是不同的

变量初始化的两种形式:

复制初始化(用等号)、直接初始化(用圆括号)。注意初始化与赋值是两种不同的操作,初始化指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替 

当初始化类类型对象时,复制初始化和直接初始化有差别。而对内置类型来说,它们几乎没有差别。记住直接初始化语法更灵活且效率更高 

内置类型变量的初始化:全局变量都初始化成 0,函数里的局部变量不进行自动初始化。

类类型变量的初始化:由构造函数来控制类对象的初始化。用户定义对象时若没有进行初始化,类会用默认构造函数来初始化。类中一般要定义默认构造函数,若类没定义默认构造函数,则定义对象时必须显示地初始化

 

 

4.动态分配2维数组

二维数组有两层的意思,第一维是一个指针数组,第二维就是第一维中每个指针所指向的数组,下面是一个实例:
  int** ptr1 = new int*[4];  //先定义第一维,他们是一个指针数组,这里是int类型。 因为是指向指针的数组所有所用了 int**
  for(int i=0;i<4;i++)
ptr1[i] = new int[4];  //第一维中的每个指针又是指向一个数组的。
 
 

5.指针数组与数组指针!

(1)指针数组:形如    int *p[10];  数组p的每一个元素都是指针!

使用:

(a)     int *p[10],int a;

P[0]=&a;

(b)     char *p[3]={“month”,”year”,”day”};

理解:指针数组反过来理解,本质就是数组,元素是指针!

(2)数组指针  形如    int (*p)[10];  p是一个指向数组的指针!

理解:数组指针反过来理解,本质是指针,指向一个数组!

 

 

6.指针函数和函数指针

(1)指针函数:形如 int *fun(int a)  即返回类型是指针!

(2)函数指针:形如 int (*fun)(int a)  即它是指向函数的!

例: int  swap(int a);

         Int (*fun)(int a);

          fun=swap;//函数名就是函数的借口首地址,形如数组!

         fun(5); 即调用了 swap(5);

7.计算机内存的4部分

(1) 程序代码区;

(2) 全程数据区:用于存储全局变量静态变量

(3) 栈区:用于存储局部变量形参变量

(4) 堆区:用于存储动态分配的数据空间!

 

总结:只有(2)(3)(4)才能用于保存数据!

 

8.字符常量的类型

“woainihaha”—> 类型是 const char *p;

所以使用strcpy(),strcat(),strcmp();时候其中的参数只能是char *pchar a[100];

这种形式的!

例:

Void main()

{

Char a[10]=”woaini”,b[10]=”wohenni”;

Strcpy(a,b);

}

 

9.产生随机数的函数

(1)产生随机数的函数:rand()  使用时包含头文件 include<stdlib>

rand()介绍: 功 :伪随机数发生器

  所属库:stdlib.h

  用 :

  需要先调用srand初始化,一般用当前日历时间初始化随机数种子,这样每次执行代码都可以产生不同的随机数。

函数原型:int rand(void)

想产生1-10的随机数:rand()%10;   想产生其他范围的数使用%就行了!

此时产生的随机数运行2次都会一样,因为它是伪随机数,所以这是我们

要调用函数srand()

 

 

(2)srand(): srand函数是随机数发生器的初始化函数。

  原型:void srand(unsigned seed);

用法:它需要提供一个种子,如: srand(1); 直接使用1来初始化种子。不过常常使用系统时间来初始化,即使用 time函数来获得系统时间,它的返回值为从 00:00:00 GMT, January 1, 1970 到现在所持续的秒数,然后将time_t型数据转化为(unsigned)型再传给srand函数,即: srand((unsigned) time(&t)); 还有一个经常用法,不需要定义time_tt变量,即: srand((unsigned) time(NULL)); (使用time 时要包含头文件 include<time.h>)直接传入一个空指针,因为你的程序中往往并不需要经过参数获得的t数据。srand((int)getpid()); 使用程序的ID(getpid())来作为初始化种子,在同一个程序中这个种子是固定的。

10.INT转换为char的函数 !

(1)函数: char *itoa(int value, char *string, int radix);   在头文件: #include<stdlib.h>

  Vaule ->要转换的数  例如 455

  string ->转换成的字符!

  radix-> 以什么方式转换! 例如:  10进制  16进制

例子:#include<iostream>

#include<stdlib.h>

using namespace std;

void main()

{

         int a=100;

         char w[10];

         itoa(a,w,10);

         cout<<w;

}

W就是 char* 类型的 !   因为数组名就是一个常量指针!

 

11.要实现多态性一定要使用virtual  !

#include<iostream>

using namespace std;

class animal

{

public:

         animal()

         {

                   cout<<"fuck"<<endl;

                   x=10;

         }

         virtual void print()  //动态调用print 的前提  使print成为virtual!

         {

                   cout<<"it is A"<<endl;

         }

private:

         int x;

};

class fish:public animal

{

public:

         fish()

         {

                   cout<<"fuck you"<<endl;

                   y=20;

         }

         void print()

         {

                   cout<<"it is B"<<endl;

         }

private:

         int y;

};

void main()

{

         fish bubble;

         animal *breath;

         breath=&bubble;

         breath->print();  //这里将显示 it is B   如果不要animal  printvirtual  此时将调用基类的print()!      将派生类赋值给基类  在使用基类调用函数 都将是 基类的函数 ,因为这样做总是安全的!  不存在 将基类赋值给派生类!   对比JAVA  将派生类赋值给基类   此时基类调用相同的重载函数都将是派生类的重载的函数 ,因为此时将屏蔽基类的相同函数!

}

 

12.delete的注意的问题!

例子:

template<class T>

void List<T>::tidyup()

{

         LinkNode<T> *p=first->link,*s=first->link,*del,*first1;

         first1=first->link->link;

         while(first1!=NULL)

         {

                   while(first1!=NULL)

                   {

                            int i=0;

                            if(s->data==first1->data)

                            {

                                     ++i;

                                     del=first1;

                                     p->link=first1->link;

                            }

                            if(i==0)

                            p=p->link;

                    first1=first1->link;

                    if(i==1)

                            delete del;//  数值相同删除节点

                   }

                   s=s->link;

                   first1=s->link;

                   p=s;

         }

}

完整代码见:E:\c++代码\数据结构试验2.

因为del  first1 都指向first,  first 又指向链表,  操作delete  del的时候会波及到first1 ,然后再操作first1 就会发生错误,所以 不能在绿色标注的带代码后 delete del  ,应该等到红色的时候 first1改变指向后 delete   ,然后操作first1  就不会出现问题!

 

深究:  当两个指针指向同一个变量的时候,改变其中任意一个都会影响到另外一个指针,就如上面的delfirst1,所以只有当其中一个指针改变指向的时候,在用delete 就不会使另外一个指针指向的值不存在而导致错误!  Delete 要和 New 搭配使用哦!

 

13.析构函数的多态性,VIRTUAL

#include<iostream>

 using namespace std;

class  A

{

public:

         A()

         {

                   a=10;

         }

         virtual ~A()

         {

                   cout<<"A析构函数被调用"<<endl;

         }

private:

         int a;

         int *p;

 

};

class B:public A

{

public:

         B()

         {

                    b=10;

         }

         ~B()

         {

                   cout<<"B 析构函数被调用"<<endl;

         }

private:

         int b;

 

};

void main()

{

         A *a=new B();

         delete a;  // 如果这里不delete a 将不会调用析构函数  因为new 分配的空间必须显示的delete

}

这里 delete a 会动态的调用 B的析构函数,显示为

 因为base 的析构函数是virtual ,先调用完派生类的析构函数,然后调用基类的析构函数!

如果没有virtual,显示为 

 

析构函数在释放对象的时候是从最内层类 释放起,然后是基类!

构造函数在构造对象的时候是从最外层类 构造起,先基类,后派生!

例如 class A{…}  class B: public A{…}       B b  此时会先调用A的构造,然后是B的构造函数!

 

14.使用初始列表能提升程序的效率

例如:

#include<iostream>

#include<string>

 using namespace std;

class  A

{

public:

         A():a(10),s("good")

         {}

private:

         int a;

         string s;

};

void mian()

{

         .....

}

A的构造函数使用了初始化列表,能调高程序效率,原因:当针对内置类型的时候没什么区别,针对类类型的时候就有区别了,初始化是发生在构造函数体的之前,如果将代码改为:

A()

{

         String s=”good”// 此时发生的是复制,将原来s中的值覆盖掉! 因为在执行函数体的时候s调用了string类的构造函数将s初始化为了空(这样:“”)!

         a=10;

}

15.派生类的拷贝函数和复制拷贝函数 注意复制每个对象!

#include<iostream>

#include<string>

using namespace std;

class A

{

public:

         A(int jy)

         {

                   a=jy;

         }

         A(const A& m)

         {

                   a=m.a;

         }

         ~A()

         {

                   cout<<"A"<<endl;

         }

private:

         int a;

 

 

};

class B:public A

{

public:

         B():A(10)

         {

                   b=10;

         }

         ~B()

         {

                   cout<<"B"<<endl;

         }

         B(const B& m):A(10),b(m.b)   //这里注意如果Adefualt 构造函数,当去掉A(10), 调用B的拷贝函数的时候会自动调用Adefault构造函数, 当有A(10),此时调用上面带A参数的构造函数!当把

A(10) 改为A(m), 此时会调用A的拷贝函数!   此时相当于A拥有了 构造函数的重载,根据实参的类型来选择调用构造函数!

         {}

private:

         int b;

};

void main()

{

        

         B b;

         B c=b;

}

看注释;

注意:当派生类在拷贝函数事 不仅要复制派生类的成员还要复制 基类的成员!

拷贝复制函数也应该注意,例如:

B&  operator=(const  B &m)

{

         A::A(m);

         b=m.b;

return (*this);

}

 

16.智能指针的解释与使用!

使用时包含头文件 include<memory>!

 

(1)     auto_pty(保存的指针类型指针名(初值)

例子:

Int *p=new int;

auto_pty<int> pu( p );

 

作用:当离开作用域后  auto_pty指针pu 会自动的释放p所分配的堆区空间!

不用显示的调用delete

注意赋值pu的时候:

例子:

        auto_pty<int> px(pu) 此时 px指向了p所动态分配的空间 pu则变为了NULL!

 

(2)     shared_pty(保存的指针类型指针名(初值)

例子同上面的差不多!

作用:与上面的auto_pty一样

但是:shared_pty的对象 在赋值的时候不会将赋值的指针设置为空,赋值后两个指针都指向同一区域!

 

17 补充12VIRTUAL的性质

一、通过父类型的指针访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。
虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:

Base1 *b1 = new Derive();
b1->f1(); //
编译出错

任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。
但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。
(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)

先来看一个问题,如果一个子类重载的虚拟函数为privete,那么通过父类的指针可以访问到它吗?

#include <IOSTREAM>   
class B   
{    
public:    
    virtual void fun()      
    {     
        std::cout << "base fun called";     
    };    
};  

class D : public B    
{    
private:   
    virtual void fun()      
    {     
        std::cout << "driver fun called";    
    };    
};  

int main(int argc, char* argv[])   
{       
    B* p = new D();    
    p->fun();    
    return 0;    
}  
运行时会输出 driver fun called

从这个实验,可以更深入的了解虚拟函数编译时的一些特征:
在编译虚拟函数调用的时候,例如p->fun(); 只是按其静态类型来处理的在这里p的类型就是B,不会考虑其实际指向的类型(动态类型)。
    也就是说,碰到p->fun();编译器就当作调用Bfun来进行相应的检查和处理。
因为在Bfunpublic的,所以这里在访问控制检查这一关就完全可以通过了。
然后就会转换成(*p->vptr[1])(p)这样的方式处理, p实际指向的动态类型是D
    
所以p作为参数传给fun(类的非静态成员函数都会编译加一个指针参数,指向调用该函数的对象,我们平常用的this就是该指针的值)实际运行时p->vptr[1]则获取到的是D::fun()的地址,也就调用了该函数这也就是动态运行的机理。


为了进一步的实验,可以将B里的fun改为private的,D里的改为public的,则编译就会出错。
C++
的注意条款中有一条绝不重新定义继承而来的缺省参数值
Effective C++ Item37 never redefine a function's inherited default parameter value) 也是同样的道理。

可以再做个实验
class B   
{    
public:   
    virtual void fun(int i = 1)      
    {     
        std::cout << "base fun called, " << i;     
    };    
};  

class D : public B    
{    
private:    
    virtual void fun(int i = 2)      
    {     
        std::cout << "driver fun called, " << i;     
    };    
}; 

则运行会输出driver fun called, 1

关于这一点,Effective上讲的很清楚“virtual 函数系动态绑定, 而缺省参数却是静态绑定
也就是说在编译的时候已经按照p的静态类型处理其默认参数了,转换成了(*p->vptr[1])(p, 1)这样的方式。 

补遗

   一个类如果有虚函数,不管是几个虚函数,都会为这个类声明一个虚函数表,这个虚表是一个含有虚函数的类的,不是说是类对象的。一个含有虚函数的类,不管有多少个数据成员,每个对象实例都有一个虚指针,在内存中,存放每个类对象的内存区,在内存区的头部都是先存放这个指针变量的(准确的说,应该是:视编译器具体情况而定),从第nn视实际情况而定)个字节才是这个对象自己的东西。

下面再说下通过基类指针,调用虚函数所发生的一切:
One *p;
p->disp();

1、上来要取得类的虚表的指针,就是要得到,虚表的地址。存放类对象的内存区的前四个字节其实就是用来存放虚表的地址的。
2
、得到虚表的地址后,从虚表那知道你调用的那个函数的入口地址。根据虚表提供的你要找的函数的地址。并调用函数;你要知道,那个虚表是一个存放指针变量的数组,并不是说,那个虚表中就是存放的虚函数的实体。

 

补充重点:你理解的是对的,子类和父类各有一个虚函数表,并且虚函数指针也是指向各自的。
子类先是从父类复制了一个虚函数表,如果子类对父类的虚函数进行了覆盖,则在子类的虚函数表将会用子类的函数地址覆盖父类的,如果没有覆盖,则还是使用父类的函数地址,这样就实现了多态。

 

 

18Delete指针后不赋予NULL的后果!

#include<iostream>

using namespace std;

void main()

{

         int *p=new int;

         *p=3;                       

         cout<<"p的值是<<*p<<endl;

         cout<<"p的地址是º?"<<p<<endl;

         delete p;

         cout<<"删除p,p的值是"<<*p<<endl;

         cout<<"删除p后,p的地址是"<<p<<endl;

         long *p1=new long;

         *p1=100;

         cout<<"p1的值是"<<*p1<<endl;

         cout<<"p1的地址是"<<p1<<endl;

         cout<<"p的地址是"<<p<<endl;

         cout<<"p的值是"<<*p<<endl;

         *p=20;

         cout<<"p1的值是"<<*p1<<endl;

}

(1)        可以看见当p被删除后,p任然有其值(编译器随机分配的),说明了编译器只是回收了为p分配的空间并没有删除P指针

(2)        然后为p1创造新的空间的时候又使用了被删除的p的空间,因为此时P没有赋予NULL,所以pp1同时指向同一块内存,改变二者任何一个都会影响到对方,所以此时会发生可怕的事情

(3)        当删除某个指针后一定要对其赋予NULL

 

 

 

 

19.当我们没有给类提供默认的构造函数时,编译器不是任何时候都提供默认构造函数!

当以下情况时才提供:

(1)       本身类或者父类或者本类包含的数据成员(类型为自定义类)有虚函数的时候,这时编译器会提供一个默认的构造函数,为了初始化虚函数表!

(2)       本身类的父类有构造函数,本身类的数据成员有构造函数,此时编译器会为本类合成一个默认构造函数,因为本身类的构造函数要调用父类或者数据成员的构造函数来初始化父类或者成员的数据!

 

当父类没有构造函数,派生类有构造函数,此时编译器不会为父类合成一个构造函数,因为这样只会浪费空间,减低效率!

 

 

注意:派生类默认的时候都只调用父类的默认构造函数(即无参构造函数)!

 

 

 

20.产生临时变量的情况!

1)给参数传递对象时(参数不是引用或者指针)

2)函数返回对象时(返回参数类型不是引用或者指针)

3)类型强制转换的时候

 

 

产生临时变量的三种情况:一:以By Value的方式传值;二:参数为const的类型。三:类型转换
一:以By Value的方式传值。
     
我们都知道,引用类型和指针类型传递的都是地址,可以直接对地址中存放的数据进行操作,
     
而以传值的方式传递参数,就会在heap中重新分配一个临时区域,
     
将实参中的数据拷贝到临时区域中,而你对这分数据进行的任何的操作都不会影响实参的内容,因为实参跟形参只是内容相同,
     
分别在两块不同的内存中。而引用和指针操作的是同一块内存,所以形参修改后,实参也修改了。
二:参数为const的类型。
     
因为常量是不能修改,在只需要实参中的数据,而不需对实参进行修改时,或是禁止对实参进行修改时,把形参定义为const类型,
     
系统会产生一个临时变量,就能起到保护数据的作用,如在函数strlen中,修改参数的值行吗?本来只是想得到实参的长度,结果在函数中被修改了,那得到得实参长度还是真实的吗。
     
如果你程序中的数据到处都可以被修改,那是多么的可怕(所以我们讨厌全局变量),所以const还是有它存在的价值。
三:类型转换的时候会产生临时变量。
     
真是糟糕啊,在用类型转换带来便利的同时,产生临时变量就是我们承担的损失。
     
如将一个short类型转换成int类型,他们占用的内存不一样,如果不产生临时变量,那不就short类型和int类型占用的字节数不就一样了吗,sizeof不就坑爹了吗

 

 

21.常量折叠

常量折叠, 
就是编译优化后, 
对于常量数据, 
就没有对应的变量存在了, 
直接就是操作这个常量数值   ... 

比如: 
const   int   X   =   5; 

那么   Y   =   2*X 
在优化后是   2*5     没有这个   X   的存在   ....

 

代码:

#include "StdAfx.h"

#include<iostream>

using namespace std;

 

 

void main()

{

          const int i=5;

         int *p=(int *)&i;

         *p=6;

         cout<<*p<<endl;

         cout<<i;

 

}

输出是:6

  5

 

调试的时候发现i的值为6!应该是编译器做了优化!

const int i=5; 改为 volatile const int i=5;

这时输出为 6  6

看看volatile的说明就懂了!

 

 

 

22.静态链接库与动态链接库  他们在VS中的引用!

静态链接库

(1)       简介

是以lib为后缀的文件,整个库中得代码最后需要连接到你的可执行文件中去,所以静态连接的可执行文件一般比较大一些,lib可以包含函数的实现代码或者只有一些关于导出函数.数据等的符号(用于隐式调用DLL)。

Visual C++的编译器在链接(有编译和链接两个过程)过程中将从静态库中恢复这些函数和数据并把他们和应用程序中的其他模块组合在一起生成可执行文件。这个过程称为"静态链接",此时因为应用程序所需的全部内容都是从库中复制了出来,所以静态库本身并不需要与可执行文件一起发行

 

(2)       使用静态链接库的方法(要用2个东西,代码要包含头文件(#include “xxx.h”),提供lib链接库)

(a)       可以再代码的前端加上:#pragma(lib,”xxx.lib”),然后包含头文件

(b)       如果使用Visual Studio,位置在 项目配置属性连接器输入附加依赖项 中加入.lib文件,然后包含头文件

(注意:上面使用的lib文件此时都在,使用lib的工程目录下面的,因为并没有在引用的时候指出lib的路径,所以只能放在使用了LIB文件的工程目录下!

 

但是也可以将lib放在任意位子,不过要让编译器能找到此lib,而不是每个硬盘每个硬盘的去寻找,所以这时要在VC++目录VS2010VC++目录被否决,解决办法看23条)下面找到库目录这个选项,加入要链接的lib的路径,这样就可以lib放到任意位子了!

 

动态链接库

(1)  简介

是以DLL为后缀的文件,库中的代码被动态的引用,所以最后不会链接到执行的文件中!

DLL里面提供了一系列的函数,变量,或者类,资源!当多个应用程序使用此DLL的时候,此DLL只会被加载到内存一次,让后映射多个应用程序,不会像静态链接库那样加载多次(当多个程序使用的时候),DLL可以包含其他的DLL(这时DLL有一个导入段)

 

2)使用动态链接库的方法(2种,隐式链接,显示链接)

(a)隐式链接

简介:此时DLL会在程序运行前由操作系统将DLL映射到虚拟地址空间中!

编程时此时要提供3个东西,头文件,生成的DLLlib  3个文件!

 

DLLlib,头文件,3个东西都放在要使用DLL的工程的默认目录下,代码中包含头文件(include “xxx.h”  然后使用lib文件(方法上面静态链接库的叙述的2种方法!)

b)显示链接

简介:只有当待用了loadlibrary()此时才将DLL映射到进程的虚拟地址空间中!

只用提供DLL文件,要使用:loadlibrary,freelibrary, GetProcAddress3个函数来调用DLL中的函数!

 

 

 

23.VC++目录被否决的解决办法!

VS2010编译器创建一个新工程,然后选择视图下的属性管理器,然后此时会出现一个属性管理器框在左侧,选择其中的Debug | win32  下面的 Microsoft.cpp.win32.use,然后弹出一个对话框,此时VC++目录就可以使用了,而且是针对所有工程,而不是单独的一个!

 

 

另外:VC++目录下的包含目录的作用:是为了能让编译器很容易的找到相应的头文件所在的路径,当我们include<xxx.h>的时候,编译器就根据包含目录下面的路径去寻找这个头文件!   如果我们的头文件在工程的默认目录下,此时就不用在包含目录中设置这个头文件的路径

 

 

Vc++目录下的库目录的作用:是为了能让编译器找到对应的lib文件的路径,因为只有头文件是不够的,lib包含了头文件中函数的实现代码!

 

 

总结:库目录和包含目录都是起到了一个指示作用,为了能让笨蛋的编译器很容易的去寻找到要使用的头文件和库!

 

 

24.预编译、编译、链接

1)预编译:预编译又称为预处理,是做些代码文本的替换工作

  处理#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等

  就是为编译做的预备工作的阶段

  主要处理#开始的预编译指令

预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。

 

2)编译:1、利用编译程序从源语言编写的源程序产生目标程序的过程。 2、用编译程序产生目标程序的动作。 编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识10,编译程序把人们熟悉的语言换成2进制的。 

 

 

3)链接: 链接器(Linker)是一个程序,将一个或多个由编译器汇编器生成的目标(obj文件)文件外加库链接为一个可执行文件exe

目标文件是包括机器码和链接器可用信息的程序模块。简单的讲,链接器的工作就是解析未定义的符号引用,将目标文件中的占位符替换为符号的地址。链接器还要完成程序中各目标文件的地址空间的组织,这可能涉及重定位工作。

 

 

 

25.一个LIB问题的解释!!(绝对有感觉)

问题

通常我们编程的时候,会包含一些头文件,像:#include<iostream>, 这时只包含了这个头文件,所以一定要有实现的代码,一般实现的代码的文件都是以libDLL结尾的文件,所以我们要在编译器中使用lib,有2种方法,一种是像:#pragram comment(lib,"xxx.lib"),一种是在附加依赖项中指定要使用的LIB


但是我只在编译器中的附加依赖项中(默认拥有,不可改变)看到了:
kernel32.lib
user32.lib
gdi32.lib
winspool.lib
comdlg32.lib........

这几个默认的LIB,并没有看到类似iostream.lib形式的链接库,那编译器是怎么知道iostream这个其中的实现代码呢?难道我上面列出的几个默认LIB,其中包含了实现iostream的代码?

我不是很清楚,请懂的朋友指点一下,先感谢!

 

 

解释:

 

 

在附加依赖项中没有上述的两个lib,编译器也成功使用了iostream头文件,这是因为编译器使用了默认库,自身就包含了此库(详见下面的叙述,并参照两个表),编译器自己寻找到他们的实现代码,不用显示的写出来!  如果是我们自己的lib文件就要自己在附加依赖项中写出来!

 

1VC2010 编译器 建立使用动态库 C++ 控制台程序,是链接 msvcrt.lib  msvcprt.lib
运行时候时候需要 msvcr100.dll msvcp100.dll (VC2010 VC 10.0)此方法是DLL的隐式使用方法,上面的22有讲解

2VC2010 编译器 建立使用静态库 C++ 控制台程序,是链接 libcmt.lib  libcpmt.lib
运行时候时候不需要 msvcr100.dll msvcp100.dll


C/C++ code

#include <iostream>

 

using namespace std;

 

int main()

{

    cout << "Hello world!" << endl;

    return 0;

}

 



动态库的编译命令 和输出大小是 7.5KB

静态库的编译命令 和输出大小是 96.00 KB ,没有 VC10的运行时的电脑上也可以执行(此时要使用的LIB中的函数代码等都已经编译进入了EXE可执行软件中)

使用动态库链接 /MD  MSVCRT.LIB 链接
cl /MD main.cpp
2012-02-28 19:46 8,704 main.exe

命令行cl默认使用静态库 /MT  LIBCMT.LIB 链接  
cl /MT main.cpp
2012-02-28 19:47 99,328 main.exe

修改静态库环境与动态库环境的方法:在项目属性中-C/C++- 代码生成-》运行库 中修改 即可!

 

 

重要:

另外,在VS的项目—>属性—>配置属性—>链接器—>输入 中有几个选项的解释:

 

(1)       附加依赖项,即是:添加lib文件,  这个LIB2个用途,1个事里面实实在在有函数,实现了一系列函数,另外一个用途是为了给DLL提供导出的函数的索引!

当我们要是使用某些DLL中的函数,就要在这个选项栏里面加上相应的lib文件的名字!

然后就不会报像这样的错:无法解析的外部符号 __imp__Add,该符号在函数 _WinMain@16 中被引用,因为我们调用了DLL中的Add()函数,前面的_imp_是编译器加上去的!

 

 

(2)       忽略所有默认库:这个选项栏的作用是:当我们在写程序的时候,使用了一些像

Windows.hiostream 这样的头文件的时候,如果只是包含头文件没有相应的实现代码是不行的,所以VS使用了一系列的默认库:比如 iostream 这个头文件使用的lib就是MSVCPRTD.LIBMSVCPRTD.LIB对应的DLL(包含实现代码)就是 MSVCP100D.DLL

 

重要:于是当我们使用iostream这个头文件的时候,我们并没有在附加依赖项中显示的添加lib文件的名字也能使用,这就是VS中提供的默认库如果我们选择忽略所有默认库,那么我们的程序会报错

 

 

另外:多个头文件,可以对应一个DLL实现库!  头文件只是声明函数,DLL才是实现函数!

比如:iostream   string 这两个头文件(C++中忽略了.h后缀)与剩余的C++头文件都是使用MSVCP100D.DLL ,对应的libMSVCPRTD.LIB! 参见下表!

 

 

C Runtime Library 参见下表:

 

可以再MSDN中搜索:C RUNTIME library   即可找到上述两个表!

猜想:

MSVCR100D.DLL 实现所有C相关的函数的代码!

MSVCP100D.DLL 实现了所有C++相关的头文件中声明函数的代码!

 

表中的option栏,可以再VS2010中的项目—>属性—>C/C++>代码生成 下的运行库选项中修改!

26为什么C++编译器不能支持对模板的分离式编译

首先,一个编译单元translation unit)是指一个.cpp文件以及它所#include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件(假定我们的平台是win32),后者拥有PEPortable Executable,即windows可执行文件)文件格式,并且本身包含的就已经是二进制码,但是不一定能够执行,因为并不保证其中一定有main函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由连接器(linker)进行连接成为一个.exe文件。

 

举个例子:

 

//---------------test.h-------------------//

void f();//这里声明一个函数f

 

//---------------test.cpp--------------//

#include”test.h”

void f()

{

…//do something

}  //这里实现出test.h中声明的f函数

 

//---------------main.cpp--------------//

#include”test.h”

int main()

{

f(); //调用ff具有外部连接类型

}

 

在这个例子中,test. cppmain.cpp各自被编译成不同的.obj文件(姑且命名为test.objmain.obj),在main.cpp中,调用了f函数,然而当编译器编译main.cpp时,它所仅仅知道的只是main.cpp中所包含的test.h文件中的一个关于void f();的声明,所以,编译器将这里的f看作外部连接类型,即认为它的函数实现代码在另一个.obj文件中,本例也就是test.obj,也就是说,main.obj中实际没有关于f函数的哪怕一行二进制代码,而这些代码实际存在于test.cpp所编译成的test.obj中。在main.obj中对f的调用只会生成一行call指令,像这样:

 

call f [C++中这个名字当然是经过mangling[处理]过的]

 

在编译时,这个call指令显然是错误的,因为main.obj中并无一行f的实现代码。那怎么办呢?这就是连接器的任务,连接器负责在其它的.obj中(本例为test.obj)寻找f的实现代码,找到以后将call f这个指令的调用地址换成实际的f的函数进入点地址。需要注意的是:连接器实际上将工程里的.obj“连接成了一个.exe文件,而它最关键的任务就是上面说的,寻找一个外部连接符号在另一个.obj中的地址,然后替换原来的虚假地址。

 

这个过程如果说的更深入就是:

 

call f这行指令其实并不是这样的,它实际上是所谓的stub,也就是一个jmp 0xABCDEF。这个地址可能是任意的,然而关键是这个地址上有一行指令来进行真正的call f动作。也就是说,这个.obj文件里面所有对f的调用都jmp向同一个地址,在后者那儿才真正”call”f。这样做的好处就是连接器修改地址时只要对后者的call XXX地址作改动就行了。但是,连接器是如何找到f的实际地址的呢(在本例中这处于test.obj中),因为.obj.exe的格式是一样的,在这样的文件中有一个符号导入表和符号导出表(import tableexport table)其中将所有符号和它们的地址关联起来。这样连接器只要在test.obj的符号导出表中寻找符号f(当然C++f作了mangling)的地址就行了,然后作一些偏移量处理后(因为是将两个.obj文件合并,当然地址会有一定的偏移,这个连接器清楚)写入main.obj中的符号导入表中f所占有的那一项即可。

 

这就是大概的过程。其中关键就是:

 

编译main.cpp时,编译器不知道f的实现,所以当碰到对它的调用时只是给出一个指示,指示连接器应该为它寻找f的实现体。这也就是说main.obj中没有关于f的任何一行二进制代码。

 

编译test.cpp时,编译器找到了f的实现。于是乎f的实现(二进制代码)出现在test.obj里。

 

连接时,连接器在test.obj中找到f的实现代码(二进制)的地址(通过符号导出表)。然后将main.obj中悬而未决的call XXX地址改成f实际的地址。完成。

 

然而,对于模板,你知道,模板函数的代码其实并不能直接编译成二进制代码,其中要有一个实例化的过程。举个例子:

 

//----------main.cpp------//

template<class T>

void f(T t)

{}

 

int main()

{

…//do something

f(10); // call f<int> 编译器在这里决定给f一个f<int>的实例

…//do other thing

}

 

也就是说,如果你在main.cpp文件中没有调用过ff也就得不到实例化,从而main.obj中也就没有关于f的任意一行二进制代码!如果你这样调用了:

 

f(10); // f<int>得以实例化出来

f(10.0); // f<double>得以实例化出来

 

这样main.obj中也就有了f<int>f<double>两个函数的二进制代码段。以此类推。

 

然而实例化要求编译器知道模板的定义,不是吗?

 

看下面的例子(将模板的声明和实现分离):

 

//-------------test.h----------------//

template<class T>

class A

{

public:

void f(); // 这里只是个声明

};

 

//---------------test.cpp-------------//

#include”test.h”

template<class T>

void A<T>::f()  // 模板的实现

{

  …//do something

}

 

//---------------main.cpp---------------//

#include”test.h”

int main()

{

A<int> a;

f(); // #1

}

 

编译器在#1处并不知道A<int>::f的定义,因为它不在test.h里面,于是编译器只好寄希望于连接器,希望它能够在其他.obj里面找到A<int>::f的实例,在本例中就是test.obj,然而,后者中真有A<int>::f的二进制代码吗?NO!!!因为C++标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来test.cpp中用到了A<int>::f了吗?没有!!所以实际上test.cpp编译出来的test.obj文件中关于A::f一行二进制代码也没有,于是连接器就傻眼了,只好给出一个连接错误。但是,如果在test.cpp中写一个函数,其中调用A<int>::f,则编译器会将其实例化出来,因为在这个点上(test.cpp中),编译器知道模板的定义,所以能够实例化,于是,test.obj的符号导出表中就有了A<int>::f这个符号的地址,于是连接器就能够完成任务。

 

关键是:在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来,所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了

 

27.位拷贝的解释

 

这个问题要考虑的是继承的情况,例如

class A

class B: public A

如果都是A的对象之间拷贝构造,那用bitwise是没有问题的

如果发生的是AB之间进行拷贝构造,两个对象的大小都不一样,就需要单独找出每一个member来拷贝,这就是memberwise

 

 

构建拷贝构造函数主要有三种方式:

一种是用户自己定义拷贝构造函数,这种方式的优先级最高,程序将优先使用这种方式复制对象,至于程序的效率取决于用户的水平;

第二是如果用户没有定义拷贝构造函数,那么系统会尝试使用bitwise copy的方式复制对象,这种方式的效率最高,编译器如果优化得比较好,将尽量采取这种方式;

最后一种,是当bitwise copy失效的情况下,编译器将自己产生拷贝构造函数,这种方法效率较低。

 

 

bitwise copy 只是简单的按位拷贝

 

memberwise copy 按成员拷贝,若成员定义了拷贝构造函数则调用拷贝构造函数<递归调用>,否则执行按位拷贝

 

 

bitwise就好像直接调用一个memcpy

memberwise copy类似调用成员变量的构造函数或者operator=

 

 

(情况12)如果一个类的成员类或者父类有拷贝构造函数, 那么这个类的拷贝构造函数就必须调用他们的copy constructor, 不能用bitwise语音构造成员类或者父类占有的那部分内存

(情况34)涉及virtual,有vptr的存在就不会使用bitwise方式

 

如果默认的构造函数是trivial的,那么就按bit-wise拷贝,否则就要调用相应数据成员构造函数。



28.vs2010编译为c代码和c++代码有何不同

问题:初用VS2010有一段时间了,我用它来编译C程序,在网上看到说在属性→C/C++→高级 对话框里面更改编译为:C代码(/TC)就可以进行C语言源码编译工作了,现在我发现有时候忘记修改这个选项也不影响我使用C来编译,有没有哪位兄弟告诉我下修改这个选项对于编译到底有什么影响?

例如:修改为编译为:C代码(/TC)和C++代码(/TP)有何区别?


解答: 

C语言和C++语言是两种不同的语言

不过C++兼容了大多C的语言特性

这个设置决定了编译器按照哪个语言规则来进行编译, 如果是C语言写的代码就用C语法规范来编译,如果

是C++语言写的就用C++语言规范来编译(:VS编译器默认怎么识别是用那种语言:如果你的源文件

是以.c结尾的就是用C语法规范编译,相应的是.cpp的就用C++语言规范!但是你可以再以.c为后缀的文件中编译写C++代码,这时会报错,不过你可以手动修改为C++语法编译,修改地方:属性→C/C++→高级-》编译为选项,这时就不会报错了!

不过如果不涉及两种语言中存在差异的语法那么用哪个编译器都是一样的


举例:

在XX.c(使用C语法规范,后缀为.c说明这是一个C源文件)中写代码:

#include <stdio.h>


struct A

{

int a;

A*B;

};


void main()

{

 A a;

}


这时默认的是以C语法规范来编译,这时就会报错!      

3种办法处理此问题:

(1)将后缀名改为.cpp

(2)手动修改C语法编译,改为C++语法编译(怎么修改上面有叙述!)

(3)修改源代码为:


#include <stdio.h>


struct A

{

int a;

struct  A*B;

};


void main()

{

struct A a;

}


29.C与C++混合编程 使用EXTERN "C" (可以百度一下extern 这个词 查看百度百科 讲解更详细)

CC++混合编程


extern "C"表示编译生成的内部符号名使用C约定。C++支持函数重载,而C不支持,两者的编译规则也不一样。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:void foo( int x, int y ); 该函数被C编译器编译后在符号库中的名字可能为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name)。_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。下面以例子说明,如何在C++中使用C的函数,或者在C中使用C++的函数。


 一://C++引用C函数的例子(C++调用Cextern "C" 的作用是:让C++连接器找调用函数的符号时采用C的方式 )


//test.c

#include <stdio.h>

void mytest()

{

printf("mytest in .c file ok\n");


}


//main.cpp


extern "C"


{


void mytest();


}


int main()


{


 mytest();


 return 0;


}


上述也可以加个头文件


//test.h


void mytest()


在后在main.cppextern "C"

{

#include “test.h”

}



二://C中引用C++函数(C调用C++,使用extern "C"则是告诉编译器把cpp文件中extern "C"定义的函数依照C的方式来编译封装接口,当然接口函数里面的C++语法还是按C++方式编译)


C中引用C++语言中的函数和变量时,C++的函数或变量要声明在extern "C"{}里,但是在C语言中不能使用extern "C",否则编译出错。(出现错误: error C2059: syntax error : 'string',这个错误在网上找了很久,国内网站没有搜到直接说明原因的,原因是extern "C"C++中的关键词,不是C的,所有会出错。那怎么用?看本文,或者搜extern "C")


//test.cpp

#include <stdio.h>

extern "C"

{

void mytest()

{

 printf("mytest in .cpp file ok\n");

}

}

//main.c

void mytest();

int main()

{

 mytest();

 return 0;

}


三.//综合使用

一般我们都将函数声明放在头文件,当我们的函数有可能被CC++使用时,我们无法确定被谁调用,使得不能确定是否要将函数声明在extern "C"里,所以,我们可以添加

#ifdef __cplusplus

 extern "C"

 {

 #endif

//函数声明

#ifdef __cplusplus

 }

 #endif


这样的话这个文件无论是被CC++调用都可以,不会出现上面的那个错误:error C2059: syntax error : 'string'

如果我们注意到,很多头文件都有这样的用法,比如string.h,等等。


//test.h

#ifdef __cplusplus


#include <iostream>

using namespace std;

 extern "C"

 {

 #endif

void mytest();

#ifdef __cplusplus

 }

 #endif


这样,可以将mytest()的实现放在.c或者.cpp文件中,可以在.c或者.cpp文件中include "test.h"后使用头文件里面的函数,而不会出现编译错误。


//test.c


#include "test.h"


void mytest()


{


#ifdef __cplusplus

cout << "cout mytest extern ok " << endl;

#else

printf("printf mytest extern ok n");

#endif

}


//main.cpp

#include "test.h"

int main()

{

 mytest();

 return 0;

}


关于C++引用C函数和变量的例子还有一个(来自网上,可以google


两个文件:


c文件:C.c
***********************************************
int external="5"; //
全局变量,缺省为extern
int func() //
全局函数,缺省为extern
{
return external;
}
***********************************************
cpp
文件:CPP.cpp
***********************************************
#include "iostream"
using namespace std;
#ifdef __cplusplus
extern "C"
{
#endif
extern int external; //告诉编译器extern是在别的文件中定义的int,这里并不会为其分配存储空间。
extern int func(); //虽然这两个都是在extern "C"{}
里,但是仍然要显式指定extern,否则报错。
#ifdef __cplusplus //
不仅仅是函数,变量也要放在extern "C"中。
}
#endif


void main(void)
{
cout<<"the value of external in c file is: "<<EXTERNAL<<ENDL;
external=10;
cout<<"after modified in cpp is : "<<FUNC()<<ENDL;
}
***********************************************


 


externC/C++语言中表明函数和全局变量作用范围(可见性)的关键字.,它告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。


1。对于extern变量来说,仅仅是一个变量的声明,其并不是在定义分配内存空间。如果该变量定义多次,会有连接错误


2。通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。也就是说c文件里面定义,如果该函数或者变量与开放给外面,则在h文件中用extern加以声明。所以外部文件只用includeh文件就可以了。而且编译阶段,外面是找不到该函数的,但是不报错。link阶段会从定义模块生成的目标代码中找到此函数。


3。与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用




30.防止包含同一个头文件的办法

有2中方法

(1)在头文件中的开头使用#pragma once  这个关键词  ,后面写上代码,这样在多次包含头文件的时候都能避免,这个事跟编译器相关的,所以有些编译器不会支持,不过VS2010支持这个关键词!


(2)使用#ifndef    #define  #endif  来避免包含同一个头文件


例子:在C++test.h中:

#ifndef  _Ctest_H

#define  _Ctest_H

int i;


void main1()

{

int b;

}

#endif



在test.h 中:
#include "C++test.h"

在C++test.cpp中:

#include "C++test.h"

#include "test.h"


void main()
{
}


这样本来就包含了C++test.h   ,然后test.h  又包含了C++test.h   如果不用#ifndef    #define  #endif 来防止多次包含同意头文件就会出错!   也可以用#pragma once;




31.分析C++的临时对象问题!

class A

 {

 public:

 A()

 {

 cout<<"构造函数"<<endl;

 }

 ~A()

 {

 cout<<"析构函数"<<endl;

 }

A(const A &a)

{

cout<<"拷贝构造函数"<<endl;

}

 A(int a)

 {

  cout<<"带参构造函数"<<endl;

 }

 };


 void main()

 {

 A a(3);   (1)

 A b=3;   (2)

                 A  c;       (3)

                 c=3;

 }

这里的(1)与(2),是两种不同的方法初始化对象! 

(1)是直接初始化,调用相应的带参构造函数!

(2)则是复制初始化,先调用相应的带参构造函数,产生一个临时对象,然后再调用拷贝构造函数使用临时对象初始化b!  但是,因为现在的编译器都可以优化,于是这个临时对象被优化掉,所以就没有调用拷贝构造函数,而只是调用了带参构造函数而已!(注意这个优化只有在构造一个新的对象的时候才会使用,像上面的(2)定义一个新的b对象!)(效果如下,析构掉b!  见(4))

(3 )这样做的话,就会产生如下图的结果,定义一个c对象,产生一次构造函数,c=3; 这时先调用带参构造函数产生一个临时对象,然后调用 operator=() 赋值操作进行赋值,然后析构掉c对象,析构掉临时对象!


(4)when a temporary class object that has not been bound to a reference (12.2) would be copied to a class object with the same cv-unqualified type, the copy operation can be omitted by constructing the temporary object directly into the target of the omitted copy

只要临时对象没有绑定到引用,并且是拷贝到一个新的对象,这个时候拷贝构造函数将被忽略,直接在这个新的目标对象中进行构造!  


(5)

class A

{

public:

A()

{

m_a=100;

cout<<"构造函数"<<endl;

}

A(int a)

{

m_a=a;

cout<<"带参构造函数"<<endl;

}

A(const A &a)

{

cout<<"拷贝构造函数"<<endl;

}

~A()

{

cout<<m_a<<"析构函数"<<endl;

}

private:

int m_a;

};

int main()

{


 A b;

 A c;

 {

A  &a=A(3);

 }

 

 cout<<"我爱你"<<endl;

 

}


效果:   

可以充分理解临时对象的问题了,参照第4点!


(6)我们可以自己优化程序,减少临时对象

class A

{

public:


A()

{

cout<<"构造函数"<<endl;

}

~A()

{

cout<<"析构函数"<<endl;

}

};

A fun()

{

        A a1

return a1;

}

void main() 

{

A a=fun();

}

像上面的代码运行结果:

fun()函数里面的a1调用了构造 与析构     main()中的a调用了析构!

本来fun()返回值应该拷贝到一个临时变量temp中, 然后再将这个临时对象temp 拷贝到a中,但是这里是初始化一个新定义的对象,于是就优化掉了这个temp 然后直接调用拷贝构造函数处理a对象!


修改一下:将main()改为


void main() 

{

A a;

a=fun();

}

运行结果:


解释: 这里不再是定义变量并且立即初始化,所以优化是得不到执行的!  于是 调用了2次构造函数,然后fun()将返回值拷贝到临时变量temp中,然后调用operator +(const A &)重载函数(未列出),赋值完成后,析构掉2个a,析构掉临时变量temp!      对比上面的解释  好好理解temp的含义!


为什么不能优化掉temp,因为这里调用了operator +(const A &)    参数const A & 要绑定到一个A类型的对象,所以不能优化掉temp! 注意!



32.类的定义与声明

一旦遇到右花括号,类的定义就结束了。并且伊尔丹定义了类,那以后我们就知道了所有的类成员,以及存储该类的对象的存储空间。在一个给定的源文件中,一个类只能被定义一次。如果在多个文件中定义一个类,那么每个文件中的类定义必须是完全相同的

将类定义放在头文件中,可以保证在每个使用类的文件中使用相同的方式定义类。使用头文件保护符(http://www.mathsky.cn/?post=73),可以保证即使头文件在同一个文件中包含多次,类定义也只会出现一次。


在普通代码中声明类类型
 
在创建类对象之前,必须完整的定义该类,必须是定义而不仅仅是声明,只有这样编译器才能给类对象分配相应的存储空间。同样的在使用引用或指针访问类的成员之前,也必须已经定义类。

在声明之后、定义之前,student类是一个不完全类型。不完全类只能以有限的方式使用:
1、不能定义该类的对象,这就是为什么需要将类定义放在头文件中
2、只能用于指向该类的指针或引用
3、只能用于声明该类型作为形参或返回类型的函数

// myfile.cpp
// 声明一个student类,定义在后面
class student; // 类的声明
// 此时student是一个不完全类,只是告诉您student是一个类,但是类的具体内容不知道
// 不完全类使用是有限的
//
student latercomer; // 错误,定义一个student类对象latercomer
student& latercomer=dynamic; //正确
double SumScore(student latercomer) // 正确
{double score=latercomer.math+latercomer.chinese;
return score;

// some other cpp code
//
// 定义student类
class student
{int age;
double math,chinese;};

在类的成员中使用类声明

当为类的成员使用类声明时,也是只有当类定义在前面出现过,数据成员才能被指定为该类类型。如果该类是不完全类声明,那么数据成员只能指向该类类型的指针或引用。
也就说只有当类定义体完成后才能定义类,因此类不能具有自身类型的数据成员。然后,只要类名一出现就可以认为该类已经声明了,因此在类的数据成员中可以指向自身类型的指针和引用。

// 定义一个类,在类中包含该类自身的引用或指针成员变量
class LinkScreen
{screen window; 
LinkScreen *next; //正确
LinkScreen prev; // 错误};

定义类类型的对象

定义一个类时,也就是定义了一种类型,一旦定义了类,就可以定义该类型的对象。定义对象时,将为其分配存储空间,但是定义类类型时不进行存储分配。

class student{}; // 定义类类型,没有分配空间
student me; // 定义类对象,分配存储空间

注意:类定义必须以分号结束,因为在类定义之后可以接一个对象定义列表,定义必须以分号结束。

class student{}; // 直接定义
class student{}me,you,she,he; // 包含定义对象列表


33.数组与指针(数组就是数组……只是大部分情况下它会隐式转换为指针而已,但是这并不是说他就是指针了

指针是C/C++语言的特色,而数组名与指针有太多的相似,甚至很多时候,数组名可以作为指针使用。于是乎,很多程序设计者就被搞糊涂了。而许多的大学老师,他们在C语言的教学过程中也错误得给学生讲解:"数组名就是指针"。很幸运,我的大学老师就是其中之一。时至今日,我日复一日地进行着C/C++项目的开发,而身边还一直充满这样的程序员,他们保留着"数组名就是指针"的误解。


  魔幻数组名

  请看程序(本文程序在WIN32平台下编译):

1. #include <iostream.h>
2. int main(int argc, char* argv[])
3. {
4.  char str[10];
5.  char *pStr = str;
6.  cout << sizeof(str) << endl;
7.  cout << sizeof(pStr) << endl;
8.  return 0;
9. }

  1、数组名不是指针

  我们先来推翻"数组名就是指针"的说法,用反证法。

  证明 数组名不是指针

  假设:数组名是指针;

  则:pStr和str都是指针;

  因为:在WIN32平台下,指针长度为4;

  所以:第6行和第7行的输出都应该为4;

  实际情况是:第6行输出10,第7行输出4;

  所以:假设不成立,数组名不是指针

  2、数组名神似指针

  上面我们已经证明了数组名的确不是指针,但是我们再看看程序的第5行。该行程序将数组名直接赋值给指针,这显得数组名又的确是个指针!

  我们还可以发现数组名显得像指针的例子:

1. #include <string.h>
2. #include <iostream.h>
3. int main(int argc, char* argv[])
4. {
5.  char str1[10] = "I Love U";
6.  char str2[10];
7.  strcpy(str2,str1);
8.  cout << "string array 1: " << str1 << endl;
9.  cout << "string array 2: " << str2 << endl;
10.  return 0;
11. }

  标准C库函数strcpy的函数原形中能接纳的两个参数都为char型指针,而我们在调用中传给它的却是两个数组名!函数输出:

string array 1: I Love U
string array 2: I Love U

  数组名再一次显得像指针!

  既然数组名不是指针,而为什么到处都把数组名当指针用?于是乎,许多程序员得出这样的结论:数组名(主)是(谓)不是指针的指针(宾)。

  整个一魔鬼。

  揭密数组名

  现在到揭露数组名本质的时候了,先给出三个结论:

  (1)数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组;

  (2)数组名的外延在于其可以转换为指向其指代实体的指针,而且是一个指针常量;

  (3)指向数组的指针则是另外一种变量类型(在WIN32平台下,长度为4),仅仅意味着数组的存放地址!

  1、数组名指代一种数据结构:数组

  现在可以解释为什么第1个程序第6行的输出为10的问题,根据结论1,数组名str的内涵为一种数据结构,即一个长度为10的char型数组,所以sizeof(str)的结果为这个数据结构占据的内存大小:10字节。

  再看:

1. int intArray[10];
2. cout << sizeof(intArray) ;

  第2行的输出结果为40(整型数组占据的内存空间大小)。

  如果C/C++程序可以这样写:

1. int[10] intArray;
2. cout << sizeof(intArray) ;

  我们就都明白了,intArray定义为int[10]这种数据结构的一个实例,可惜啊,C/C++目前并不支持这种定义方式。

  2、数组名可作为指针常量

  根据结论2,数组名可以转换为指向其指代实体的指针,所以程序1中的第5行数组名直接赋值给指针,程序2第7行直接将数组名作为指针形参都可成立。

  下面的程序成立吗?

1. int intArray[10];
2. intArray++;

  读者可以编译之,发现编译出错。原因在于,虽然数组名可以转换为指向其指代实体的指针,但是它只能被看作一个指针常量,不能被修改。

  而指针,不管是指向结构体、数组还是基本数据类型的指针,都不包含原始数据结构的内涵,在WIN32平台下,sizeof操作的结果都是4。
顺便纠正一下许多程序员的另一个误解。许多程序员以为sizeof是一个函数,而实际上,它是一个操作符,不过其使用方式看起来的确太像一个函数了。语句sizeof(int)就可以说明sizeof的确不是一个函数,因为函数接纳形参(一个变量),世界上没有一个C/C++函数接纳一个数据类型(如int)为"形参"。

  3、数据名可能失去其数据结构内涵

  到这里似乎数组名魔幻问题已经宣告圆满解决,但是平静的湖面上却再次掀起波浪。请看下面一段程序:

1. #include <iostream.h>
2. void arrayTest(char str[])
3. {
4.  cout << sizeof(str) << endl;
5. }
6. int main(int argc, char* argv[])
7. {
8.  char str1[10] = "I Love U";
9.  arrayTest(str1);
10.  return 0;
11. }

  程序的输出结果为4。不可能吧?

  一个可怕的数字,前面已经提到其为指针的长度!

  结论1指出,数据名内涵为数组这种数据结构,在arrayTest函数体内,str是数组名,那为什么sizeof的结果却是指针的长度?这是因为:

  (1)数组名作为函数形参时,在函数体内,其失去了本身的内涵,仅仅只是一个指针;

  (2)很遗憾,在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。

  所以,数据名作为函数形参时,其全面沦落为一个普通指针!它的贵族身份被剥夺,成了一个地地道道的只拥有4个字节的平民。

  以上就是结论4。

 


  中的字符串字面量 "hello" 是数组类型 char[6](相应地,每个字符元素是无 const 限定的 char 型);作为右值使用的时候转换为指针类型 char*

  在 C++  "hello"  char const [6] 类型(相应地,每个字符元素的类型是 char const);转换为指针使用的时候是 char const*、在特殊情况下也可以是 char*

重要:数组到指针的转换规则只适用于表达式

当作为数组对象对数组的引用进行初始化、及作为sizeof、&的操作数时,数组才不会被转换为指针,代表数组聚合对象。除此之外,它被转换为一个非对象的符号地址 ,在表达式中数组名变为一个指针!(所以int a[40],sizeof(a)=40,此时数组名a是代表聚合对象)!

而:

int main()

{

char a[40];

int i=sizeof(a+1);

cout<<i;


}

此时a出现在表达式中,转换为指针,所以i为4;

问答:

问:这么说“数组名出现在表达式中被转化为指针时”,不是被转化为了“不可修改的左值”,而是直接转化为了右值

答:对,数组名在经过数组到指针的转换后,就是个右值。否则,是一个不可修改的左值。



34。为什么a与&a的地址值相同?(int a[10])

上面说了,a出现在表达式中时(a+1),它作为指针,其值是数组第一个元素的地址;而当a作为取址(&)操作的操作数时,取址操作返回的是指向数组的指针。由于数组的地址和数组第一个元素的地址相同,所以a与&a的地址值相同。
--------------------------------------------
这只是表面现象,有没有思考过&a是否合法?一般来说,&运算符要求操作数是对象,但,数组名是对象吗?
先给你解释什么是对象,对象是执行环境中其内容表示为某个值的存储数据的区域。

当数组名出现在&之后时,编译器把它理解为a[0],而a[0]是合法的可用于&运算符的对象。
----------------------------------------------------------------------
如果定义int i; i是一个对象,&i成立是很自然的,但数组名不是对象(数组是对象),因为内存中并不存在一个地方存储数组名的值,数组名本身就代表一个值,没有指针对象那样的取值操作,因此按照&的本意,&a是非法的,但是由于早期有些编译器已经允许了&a这样的东西的存在,因此C标准委员会鉴于这种结构并没有害处而且对象的概念已经有了扩展,因此也规定了&a的合法性,但&a并不表示对数组名取地址,而是对数组对象取地址,也正因为这个原因,&a的值跟a是一样的


35.维数组与指针的指针

 char a[5][7];
    char (*b)[7];
    char **p;
把a理解为一个数组的数组,它可以赋值给指向数组的指针b,但是不能赋值给指向指针的指针



36.单引号字符与sizeof的讨论

int main()  

{  

int a1='a';

char a2='a';

cout<<a1<<"   "<<a2<<"   "<<sizeof('a');

}

运行结果:

解释:char 字符在赋值给int类型的时候可以转化为 int类型!    所以上面的字符使用的ASCII编码,所以'a'对应的值为97, sizeof('a')即是(sizeof(char))所以为1;



int main()  

{  

int a1='中';

char a2='中';

cout<<a1<<"   "<<a2<<"   "<<sizeof('中');

}

运行结果:

char a2='中'; 这行报警告,从“int”到“char”截断!

“初始化”: 截断常量值


解释:这时'中'这个字符使用的不在是ASCII编码了(最大能表示0-255),应该是GB2312编码! 

所以sizeof('中') 即是 sizoef(int) 所以是4!


int main()  

{  

char a[]="中国";

cout<<sizeof(a);

        cout<<sizeof("中国");

}

结果:
解释:
因为: 中国  这两个字符是2个字节,  然后是结尾符号'\0', 加起来就是 2*2+1=5;
cout<<sizeof("中国");  “中国” 的类型是 const char []  !  不是const char *;   




int main()  
{  
wchar_t a[]=L"中国";
cout<<sizeof(a);
}


结果:

解释: 

这时是 wchar_t 类型的  是宽字符 ,所以不论事中文 还是英文 编码都是2个字节!



37.  :: 这个符号要到的地方展示!



代码:

namespace my_std

{

class A

{

public:

static void show();

static int first_member;


class B

{

public:

   typedef int  _INT;

};

};


void A::show()                (1)

{


}

}


int main()  

{  

        my_std::A;                         (2)

my_std::A::B;                     (3)

my_std::A::first_member;   (4)

my_std::A::show();             

my_std::A::B::_INT a;          (5)

a=10;                   

cout<<a;

}


说明:

(1)当以一个类中声明了一个函数,在类的外部定义这个函数的时候,要用::说明这个函数是属于哪个类的!

(2)当一个类在一个命名空间里面的时候,要使用这个类,必须说明是在哪个命名空间里面!

(3)当一个类中含有一个类的时候(这里是A含有B,嵌套类),要使用::说明!

(4)当在一个类中定义了静态成员函数,或者静态变量的时候,可以用::指明,也可以定义了对象再指明

(5)当在一个类中(这里是B)typedef int  _INT;   这里用::说明是在这个B这个类中定义的名字!(在STL用到了很多这种类似的定义!)



38、Typename 这个关键字的两个作用!


(1)在 template<typename T>  指明T是模版参数


(2)

template <typename T>

class C

{

public:

typename my_std::A::B::_INT *p_Int;  (注:参见37)


};


说明_INT  是my_std::A::B  中定义的类型名,因此 p_Int是一个指向 my_std::A::B::_INT  类型的指针。如果没有typename ,_INT会被当成以个static成员



39.this指针的认识

下面这个程序不见得在每种编译器上都能通过编译,既使通过了编译,也不见得可以成功运行并输出结果。

让人欣慰,更让人郁闷的是:它在我的Visual Studio 2005 SP1中,既能顺利编译,也能正常运行。

它虽然投机取巧,胡作非为,蝇营狗苟,横行霸道。但它至少能引发你的思考,加深一点你对this指针的理解,如果你还不是十分理解的话。

[cpp] view plaincopy
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. #pragma warning(disable:4213)  
  5.   
  6. class A {  
  7. public:  
  8.     int i;  
  9.     A() : i(0) {}  
  10.     void f(A&);  
  11. };  
  12.   
  13. void A::f(A& o){  
  14.     reinterpret_cast<A*>(this) = &o;  
  15.     i = 100;  
  16. }  
  17.   
  18. int main() {  
  19.     A a, b;  
  20.     a.f(b);  
  21.     cout << "a.i = " << a.i << endl;  
  22.     cout << "b.i = " << b.i << endl;  

输出结果是  0,100 

成员函数都具有一个附加的隐藏this指针,成员函数不能定义this形参,而是由编译器隐含的定义。可以显示的使用this指针!  this指向的是调用此成员函数的对象!

非CONST 的函数中的this的类型: T * CONST this!

在const函数中,this的类型是: const T *const  this   !


记住: 调用非静态的成员函数的时候,编译器要传递一个隐含的this指针,我们要清楚这个this是指向什么对象的!

列一:

class A

    void show()

{}

void main(){A::show() } //错误  ,这里的没有传递一个this指针所以会报

错! 

例二:






40.文件的结束符号与文件的换行符号

(1)文件的结束符

声明: 根本没有什么文件结束符!     所谓的EOF并不是什么文件结束符!  

EOF 被 #define EOF -1 

当我们调用读取文件的API的时候,函数就不停地读,当它发现到达文件的结尾时候,他就会自动停止!如果返回-1 (见)则就是所谓的EOF, 帮助我们程序员知道 文件读到结尾了!

如果不信,可以新建一个文件夹,什么都不输入,查看它的大小,你会发现是0,或者当我们输入abc,你会发现它的大小是3,  所以可以知道根本没有文件结束符号!


: readfile()     返回值: 读取成功 返回非0,失败 返回 0;

       getchar()  作用:输入流中读取   返回值:Returns the character read. To indicate a read error or end-of-file condition, getchar returns EOF, and getwchar returns WEOF


       fgetc ()   作用:输入流中读取   返回值:fgetc returns the character read as an int or returns EOF to indicate an error or end of file.

都说明了EOF是一个返回值!不是文件结尾标识符!


(2)文件换行符

a.当我们打开一个记事本(默认就是文本文件),我们不停地输入,输多了他就会自动换行,注意:这是因为屏幕一行显示不到了所以就换行了,这里并没有插入换行符号!  会 如果的你屏幕无限宽,那么永远都是一行显示!

b. 还是打开一个记事本,当我们输入 一个 a, 然后按回车,在输入一个b,如图:

这是我们查看这个记事本大小,可看到是4个字节大!   所以可知 换行符 是2个字节!

在window下 换行符(2个字符)就是 对应的 13  10!


调用函数读写文件的时候要注意:

情形一:

当我们是文本文件 方式写入文件的时候,  char a=10; (10是换行的ASCII码值,这里会隐士转换为char类型)  比如这里写入 这个a  ,我们会发现这个被写入的文件大小是2,为什么呢?  因为windows 会将    a保存的这个换行 ASCII值  替换成   13  10 这两个ASCII码值! 

当我们以文本文件方式读入文件的时候,读取来只有1个字符 ,这个字符所对应的ASCII值就是10!

(这是因为是以文本文件的方式读入,所以当发现时13 10这两个ASCII码的时候,就转换为一个ASCII码 10)

当我们以 二进制方式读入文件的时候,读取来只有2个字符,这两个字符所对应的ASCII值就是 13 10!


情形二:

 当我们是 二进制方式    写入文件的时候, char a=10;   这是文件的大小是1!

当我们以文本文件方式读入文件的时候, 读取来只有1个字符 ,这个字符所对应的ASCII值就是10!

当我们以 二进制方式读入文件的时候,读取来只有1个字符 ,这个字符所对应的ASCII值就是10!


情形三:

当我们打开一个记事本输入   一个 a, 然后按回车,在输入一个b,如图:

然后以: 文本方式读入,发现能读出3个字符!

以二进制方式读入,能读出4个字符!


注:记事本的默认编码方式为:ANSI , 对应到中国的就是GB2312 

GB2312编码方式:

当然对于ANSI编码而言,0x00~0x7F(0-127)之间的字符,依旧是1个字节代表1个字符

通常使用 0x80~0xFF (128-255)范围的 2 个字节来表示 1 个字符。比如:汉字 '中' 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储


41.ASCII码  与 字符(分为:可打印和不可打印)


像我们平时写的  a b c d。。。 等这些字符,要在计算机上面保存(因为计算机只认识 0和1)所以必须有编码方式,

所以标准ASCII 采用 7位编码所以范围就是 0-127   (十进制)  ,这里面的0-127范围的数字分别对应一个字符,这些字符分为可打印与不可打印(不可显示的),  可打印的像: 'a'   'b'  '9'  等,不可打印的有:


0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符),如控制符:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(振铃)等;通信专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等;ASCII值为8、9、10 和13 分别转换为退格、制表、换行和回车字符。它们并没有特定的图形显示,但会依不同的应用程序,而对文本显示有不同的影响。


转义字符”所有的ASCII码都可以用“\”加数字(一般是8进制数字)来表示。而C中定义了一些字母前加"\"来表示常见的那些不能显示的ASCII字符,如\0,\t,\n等,就称为转义字符,因为后面的字符,都不是它本来的ASCII字符意思了。

所以为了能表示这些不可打印的字符和特殊字符,所以就使用转义字符书写:

像换行符号: char a='\n'    这样就可以显示输入换行符了!  或者  char a='\12'   (12是八进制)   也可以表示换行符!(用八进制可以表示所有ASCII字符)     、  或者 char a=10;    也可以表示换行符  这里将 int 转换为了  char类型!  

换行符对应的ASCII 码为10!



执行时间:



42.指向类中的成员与类中的成员函数的指针!

前面曾写过一篇恼人的函数指针(一),总结了普通函数指针的声明、定义以及调用,还有函数指针数组,函数指针用作返回值等。但是作为C++的研读,我发现我漏掉了一个最重要的内容,就是指向类成员的指针,这里将做相应补充(相关代码测试环境为vs 2010)。

指向类成员的指针总的来讲可以分为两大类四小类(指向数据成员还是成员函数,指向普通成员还是静态成员),下面一一做介绍:

一、指向类的普通成员的指针(非静态

1、指向类成员函数的指针

简单的讲,指向类成员函数的指针与普通函数指针的区别在于,前者不仅要匹配函数的参数类型和个数以及返回值类型,还要匹配该函数指针所属的类类型。总结一下,比较以下几点:

a)参数类型和个数

b)返回值类型

c)所属的类类型(特别之处)

究其原因,是因为非静态的成员函数必须被绑定到一个类的对象或者指针上,才能得到被调用对象的this指针,然后才能调用指针所指的成员函数(我们知道,所有类的对象都有自己数据成员的拷贝,但是成员函数都是共用的,为了区分是谁调用了成员函数,就必须有this指针,this指针是隐式的添加到函数参数列表里去的)。

明白了这点,接下来就简单了。

声明:与普通函数作为区分,指向类的成员函数的指针只需要在指针前加上类类型即可,格式为:

typedef 返回值 (类名::*指针类型名)(参数列表);

赋值:只需要用类的成员函数地址赋值即可,格式为:

指针类型名  指针名 = &类名::成员函数名;

注意:这里的这个&符号是比较重要的:不加&,编译器会认为是在这里调用成员函数,所以需要给出参数列表,否则会报错;加了&,才认为是要获取函数指针。这是C++专门做了区别对待。

调用:调用方法也很简单,针对调用的对象是对象还是指针,分别用.*和->*进行调用,格式为:

(类对象.*指针名)(参数列表);

(类指针->*指针名)(参数列表);

注意:这里的前面一对括号是很重要的,因为()的优先级高于成员操作符指针的优先级。

下面举个简单的例子就一目了然了:

 1 class A;
2 typedef void (A::*NONSTATICFUNCPTR)(int); //typedef
3
4 class A
5 {
6 public:
7 void NonStaticFunc(int arg)
8 {
9 nonStaticMember = arg;
10 cout<<nonStaticMember<<endl;
11 }
12 private:
13 int nonStaticMember;
14 };
15
16 int main()
17 {
18 NONSTATICFUNCPTR funcPtr= &A::NonStaticFunc;
19
20 A a;
21 (a.*funcPtr)(10); //通过对象调用
22
23 A *aPtr = new A;
24 (aPtr->*funcPtr)(10); //通过指针调用
25
26 return 0;
27 }

2、指向类数据成员的指针

成员函数搞懂了,数据成员也就easy了,只要判断以下两点是否一致即可:

a)数据成员类型

b)所属的类类型

另外,声明、赋值还有调用方法等这些是和前面类似的,再举个例子吧:

 1 class A;
2 typedef int (A::*NONSTATICDATAPTR); //typedef
3
4 class A
5 {
6 public:
7 A(int arg):nonStaticMember(arg){}
8 int nonStaticMember;
9 };
10
11 int main()
12 {
13 NONSTATICDATAPTR dataPtr= &A::nonStaticMember;
14
15 A a(10);
16 cout<<a.*dataPtr; //通过对象引用
17
18 A *aPtr = new A(100);
19 cout<<aPtr->*dataPtr; //通过指针引用
20
21 return 0;
22 }

运行结果,当然是各自输出10和100啦。

二、指向类的静态成员的指针

类的静态成员和普通成员的区别在于,他们是不依赖于具体对象的,所有实例化的对象都共享同一个静态成员,所以静态成员也没有this指针的概念。

所以,指向类的静态成员的指针就是普通的指针

看下面的例子就明白了:

 1 typedef const int *STATICDATAPTR;    
2 typedef int (*STATICFUNCPTR)(); //跟普通函数指针是一样的
3
4 class A
5 {
6 public:
7 static int StaticFunc() { return staticMember; };
8 static const int staticMember = 10;
9 };
10
11 int main()
12 {
13 STATICDATAPTR dataPtr = &A::staticMember;
14 STATICFUNCPTR funcPtr = &A::StaticFunc;
15
16 cout<<*dataPtr; //直接解引用
17 cout<<(*funcPtr)();
18
19 return 0;
20 }

最后注明一下,显然的,要使用(&类名::成员名)获取指向成员的指针,首先这个成员必须是对外可见的哦,即public的,不然是没有权限获取的^^。


写到此,简单总结一下就是:

1)静态的和普通的函数指针没啥区别;

2)非静态的加一个类局限一下即可。

 

不知道以后还会不会有函数指针相关的内容,先到此完结吧。

有错误欢迎指正,我会及时修改^^。




43.::操作符的问题


class test

{

public:

void show()

{

}

void fun()

{

  test::show();           //不会报错,  没有错,因为调用fun()这个函数的时候就会传递一个this指针,此时调用show(),也会保证有一个this指针的传递!

}

};


int main(int argc,char argv[]) 

{

  

  test a;

  test::show();                      //将会报错,原因调用show()要传递一个隐式的this指针,这里没有传递this指针,形如:show(const test *)

          a.show()        这样不会报错,this指针指向a这个对象!  show(&a);

}



44.重载操作符的问题!

 class A

{

public:

                

void operator+(const A & rhs)        //(1)

{

cout<<"A 类中的+"<<endl;

}

};


void   operator+(const A &lhs,const A &rhs)     (2)

{

cout<<"A 类外的+ "<<endl;

}


int main(int argc,char argv[]) 

{

  A a,b;

        a+b;     当(1)(2)都存在的时候, 调用(1),当删除了(1)的代码,将会调用(2)

}


(2)因为使用了A 类型作为的参数 , 就成为了A 中的接口!


45.C/C++程序编译步骤详解


C/C++语言很多人都比较熟悉,这基本上是每位大学生必学的一门编程语言,通常还都是作为程序设计入门语言学的,并且课程大多安排在大一。刚上大学,孩子们还都很乖,学习也比较认真,用心。所以,C/C++语言掌握地也都不错,不用说编译程序,就是写个上几百行的程序都不在话下,但是他们真的知道C/C++程序编译的步骤么?

我想很多人都不甚清楚,如果他接下来学过“编译原理”,也许能说个大概。VC的“舒适”开发环境屏蔽了很多编译的细节,这无疑降低了初学者的入门门槛,但是也“剥夺”了他们“知其所以然”的权利,致使很多东西只能死记硬背,遇到相关问题就“丈二”。实际上,我也是在学习Linux环境下编程的过程中才逐渐弄清楚C/C++源代码是如何一步步变成可执行文件的。

总体来说,C/C++源代码要经过:预处理编译汇编连接四步才能变成相应平台下的可执行文件。大多数时候,程序员通过一个命令就能完成上述四个步骤。比如下面这段C的“Hello world!”代码:

File: hw.c

#include <stdio.h>

int main(int argc, char *argv[])
{
        printf("Hello World!\n");

        return 0;
}


如果用gcc编译,只需要一个命令就可以生成可执行文件hw:

xiaosuo@gentux hw $ gcc -o hw hw.c

xiaosuo@gentux hw $ ./hw Hello World!


我们可以用-v参数来看看gcc到底在背后都做了些什么动作:

Reading specs from /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/specs
Configured with: /var/tmp/portage/sys-devel/gcc-3.4.6-r2/work/gcc-3.4.6/configure--prefix=/usr --bindir=/usr/i686-pc-linux-gnu/gcc-bin/3.4.6--includedir=/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/include--datadir=/usr/share/gcc-data/i686-pc-linux-gnu/3.4.6--mandir=/usr/share/gcc-data/i686-pc-linux-gnu/3.4.6/man--infodir=/usr/share/gcc-data/i686-pc-linux-gnu/3.4.6/info--with-gxx-include-dir=/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/include/g++-v3--host=i686-pc-linux-gnu --build=i686-pc-linux-gnu --disable-altivec --enable-nls--without-included-gettext --with-system-zlib --disable-checking --disable-werror--enable-secureplt --disable-libunwind-exceptions --disable-multilib --disable-libgcj--enable-languages=c,c++,f77 --enable-shared --enable-threads=posix--enable-__cxa_atexit --enable-clocale=gnu
Thread model: posix
gcc version 3.4.(Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.10)
 /usr/libexec/gcc/i686-pc-linux-gnu/3.4.6/cc1 -quiet -v hw.-quiet -dumpbase hw.c-mtune=pentiumpro -auxbase hw -version -o /tmp/ccYB6UwR.s
ignoring nonexistent directory "/usr/local/include"
ignoring nonexistent directory "/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../../i686-pc-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/include
 /usr/include
End of search list.
GNU C version 3.4.(Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.10) (i686-pc-linux-gnu)
        compiled by GNU C version 3.4.(Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.9).
GGC heuristics: --param ggc-min-expand=81 --param ggc-min-heapsize=97004
 /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../../i686-pc-linux-gnu/bin/as --Qy -o /tmp/ccq8uGED.o /tmp/ccYB6UwR.s
GNU assembler version 2.17 (i686-pc-linux-gnu) using BFD version 2.17
 /usr/libexec/gcc/i686-pc-linux-gnu/3.4.6/collect2 --eh-frame-hdr -m elf_i386-dynamic-linker /lib/ld-linux.so.-o hw /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../crt1.o /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../crti.o /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtbegin.o-L/usr/lib/gcc/i686-pc-linux-gnu/3.4.-L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6-L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../../i686-pc-linux-gnu/lib-L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../.. /tmp/ccq8uGED.-lgcc --as-needed-lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtend.o /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../crtn.o


稍微整理一下,去掉一些冗余信息后,如下:

cc1 hw.-o /tmp/ccYB6UwR.s
as -o /tmp/ccq8uGED.o /tmp/ccYB6UwR.s
ld -o hw /tmp/ccq8uGED.o


以上三个命令分别对应于编译步骤中的预处理+编译、汇编和连接。预处理和编译还是放在了一个命令(cc1)中进行的,可以把它再次拆分为以下两步:

cpp -o hw.i hw.c
cc1 hw.-o /tmp/ccYB6UwR.s


一个精简过的能编译以上hw.c文件的Makefile如下:

.PHONY: clean

all: hw

hw: hw.o
        ld -dynamic-linker /lib/ld-linux.so.-o hw /usr/lib/crt1.o \
                /usr/lib/crti.o \
                /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtbegin.o \
                hw.-lc \
                /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtend.o \
                /usr/lib/crtn.o

hw.o: hw.s
        as -o hw.o hw.s

hw.s: hw.i
        /usr/libexec/gcc/i686-pc-linux-gnu/3.4.6/cc1 -o hw.s hw.c

hw.i: hw.c
        cpp -o hw.i hw.c

clean:
        rm -rf hw.i hw.s hw.o


当然,上面Makefile中的一些路径是我系统上的具体情况,你的可能与我的不同。

接下来我们按照编译顺序看看编译器每一步都做了什么。

首先是预处理,预处理后的文件hw.i:

# 1 "hw.c"
# 1 "<built-in>"
# 1 "<command line>"

...
__extension__ typedef __quad_t __off64_t;
__extension__ typedef int __pid_t;
__extension__ typedef struct { int __val[2]; } __fsid_t;

...
extern int remove (__const char *__filename) __attribute__ ((__nothrow__));

extern int rename (__const char *__old, __const char *__new) __attribute__((__nothrow__));

...

int main(int argc, char *argv[])
{
 printf("Hello World!\n");

 return 0;
}


:由于文件比较大,所以只留下了少部分具有代表性的内容。

可以看见预处理器把所有要包含(include)的文件(包括递归包含的文件)的内容都添加到了原始的C源文件中,然后把其输出到输出文件,除此之外,它还展开了所有的宏定义,所以在预处理器的输出文件中你将找不到任何宏。这也提供了一个查看宏展开结果的简便方法。

第二步“编译”,就是把C/C++代码“翻译”成汇编代码:

.file "hw.c"
        .section .rodata
.LC0:
        .string "Hello World!\n"
        .text
.globl main
        .type main, @function
main:
        pushl %ebp
        movl %esp, %ebp
        subl $8, %esp
        andl $-16, %esp
        movl $0, %eax
        addl $15, %eax
        addl $15, %eax
        shrl $4, %eax
        sall $4, %eax
        subl %eax, %esp
        subl $12, %esp
        pushl $.LC0
        call printf
        addl $16, %esp
        movl $0, %eax
        leave
        ret
        .size main, .-main
        .section .note.GNU-stack,"",@progbits
        .ident "GCC: (GNU) 3.4.6 (Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.10)"


这个汇编文件比预处理后的C/C++文件小了很多,去除了很多不必要的东西,比如说没用到的类型声明和函数声明等。

第三步“汇编”,将第二步输出的汇编代码翻译成符合一定格式的机器代码,在Linux上一般表现为ELF目标文件。

xiaosuo@gentux hw $ file hw.o
hw.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped


最后一步“连接”,将上步生成的目标文件和系统库的目标文件和库文件连接起来,最终生成了可以在特定平台运行的可执行文件。为什么还要连接系统库中的某些目标文件(crt1.o, crti.o等)呢?这些目标文件都是用来初始化或者回收C运行时环境的,比如说堆内存分配上下文环境的初始化等,实际上crt也正是C RunTime的缩写。这也暗示了另外一点:程序并不是从main函数开始执行的,而是从crt中的某个入口开始的,在Linux上此入口是_start。以上Makefile生成的是动态连接的可执行文件,如果要生成静态连接的可执行文件需要将Makefile中的相应段修改:

hw: hw.o
    ld -m elf_i386 -static -o hw /usr/lib/crt1.o \
        /usr/lib/crti.o \
        /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtbeginT.o \
        -L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6 \
        -L/usr/i686-pc-linux-gnu/lib \
        -L/usr/lib/ \
        hw.--start-group -lgcc -lgcc_eh -lc --end-group \
        /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtend.o \
        /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../crtn.o


至此,一个可执行文件才最终创建完成。通常的项目中并不需要把编译过程分得如此之细,前三步一般是合为一体的,在Makefile中表现如下:

hw.o: hw.c
    gcc -o hw.-c hw.c


实际上,如果对hw.c进行了什么更改,那么前三步大多数情况下都是不可避免的。所以把他们写在一起也并没有什么坏处,相反倒可以用--pipe参数告诉编译器用管道替代临时文件,从而提升编译的效率



46.动态类型和静态类型

C++ 标准明确定义:
1.3.3 dynamic type [defns.dynamic.type]
the type of the most derived object (1.8) to which the lvalue denoted by an lvalue expression refers.
The dynamic type of an rvalue expression is its static type.
动态类型的定义是:由一个左值表达式指出的左值的动态类型,是其所引用的对象的最狭义类型。一个右值表达式的动态类型,就是它的静态类型。

1.3.11 static type [defns.static.type]
the type of an expression (3.9), which type results from analysis of the program without considering execution semantics. The static type of an expression depends only on the form of the program in which the expression appears, and does not change while the program is executing.
静态类型,是指不需要考虑表达式的执行期语义,仅从表达式的字面的形式就能够决定的类型。静态类型在程序运行时不会改变。

通常我们说,“基类指针指向的对象的实际/真正类型”或“基类引用所引用的对象的实际/真正类型”,就是它们的动态类型。很显然,这个动态类型是  C++ 语言通过指针和引用实现运行时多态能力的核心概念。

我将在考虑有关左值和右值的概念时继续对动态类型和静态类型的语义逻辑进行分析。


47.sizeof() 与 typeid()



sizeof()求的是静态类型  

typeid():如果表达式的类型是类类型且至少包含有一个虚函数,则typeid操作符返回表达式的动态类型,需要在运行时计算;否则,typeid操作符返回表达式的静态类型,在编译时就可以计算。

 class Father

{

public:

void show()

{

cout<<"Father Show()"<<endl;

}

void fun()

{

show();

}

};

class Son : public Father

{   

public:

void show()

{

cout<<"Son show()"<<endl;

}

};

int main(int argc,char argv[]) 

{

Father *p=new Son;

cout<<typeid(*p).name()<<endl;

cout<<sizeof(*p);

}

输出:


当巴father中的show()改为virtual 的时候  这时的结果是:




sizeof()求的始终都是  father的大小,因为这才是p的类型!


而typeid() 在无虚函数下求的是静态类型,在有虚函数下求的是 动态类型!(this指针同解~~)



48.c++不允许修改临时变量的值

例子:


int fun(int a)

{

return a;

}

void play(int a)

{

cout<<a;

}


int main()

{


play(++fun(3));


}


报错:“++”需要左值



49.恶搞C++


typedef int UINT4;

using namespace std;

class Hack

{

};

Hack& operator< (Hack &a , Hack &b)

{

std::cerr << "小于操作符\n";

return a;

}


Hack& operator> (Hack &a, Hack &b)

{

std::cerr <<  "大于操作符\n";

return a;

}


int main(int argc, char ** argv)

{

Hack vector;

Hack UINT4;

Hack foo;


vector<UINT4> foo;   // 这里不在时容器的vector 而是 被解释成 :

 operator>(operator<(vector,UINT4),foo);

return(0);

}



50.一段程序想到的问题

int main()

{

unsigned i=0x30D2CE;

char *p=(char*)&i;

cout<<p;

}

输出结果:

0x30 : 10     数字是1个字节编码

0xCED2:我  中文是2个字节编码

 

解释: 对应的16进制为:   编码方式:GB2312


因为使用的是intel的处理器,这种处理器的在编码上面的i的值的时候是小端保存,所以我手动将  "我0"  的16进制反向赋值(0x30D2CE)给i,这时在内存中保存的 就是相应的"CE D2 30"!

 于是将 i的首地址(int是4个字节,总共连续4个地址,1个地址对应1个字节)强制转换后为char* 赋值给p!

作用:变量的前面的类型说明符的目的就是为了,让内存知道怎么解释变量里面的内容,并且知道是变量几个字节!

所以强制转换后,本来是按4个字节(对应4个地址),并且按照无符号整形来解释! 但是转换后就是按照:1个或2个字节(GB2312编码),并且按相应的GB2312解码(因为编码方式是GB2312),来解释相应的16进制(即是2进制)所对应的字符!

GB2312:是按照:0-127数值的编码是按照ASCII编码原来的方式,128-255里面的字符 选用2个数 组合来进行编码 来表示一个中文!    


再看下面的程序:

  

对应的16进制为:   编码方式是:unicode

为什么这里有6个字节,本应该有4个字节,前面的FF FE的作用是:说明这是Unicode编码!


int main()

{

    unsigned i=0x00306211;

    char *p=(char*)&i;

    cout<<p;

}

结果:    , 为什么呢?   原因:这里采用的是unicode编码,对应的应该使用whcar_t!  而这里使用的是char ,于是按照GB2312来进行解码(解释2进制数的含义),所以输出的东西不是我们想要的!


修改一下:

int main()

{

unsigned i=0x00306211;

wchar_t *p=(wchar_t*)&i;

cout<<p;

}

输出是:


为什么这里是:输出的地址,而不是像char* 那样输出里面的字符串,我想是因为: cout这个对象的类 basic_ostream 中对

operator<< 重载的实现 有所不同所造成的! 所以char*输出的是字符串,wchar_t* 输出的是地址!


没办法  ,进行调试可以看到:

前面的2个字符是:我0,所以目的达到!



另外可以百度看看:  记事本保存: 联通    2个字符的问题!

简述原因:  记事本默认保存(默认选择的是ANSI,在中国就是对应的GB2312)的是GB2312!  但是当我们打开这个文件的时候为什么会出现乱码的问题呢?      因为打开的时候,记事本会判断采用的是什么编码方式(只能用猜测的办法,猜测是有依据的,比如前面的Unicode编码的文件,文件开头是FF FE 开始的) ,这里由于记事本检测到 联通 编码,像UTF-8的开头, 所以就误认为这是UTF-8编码,于是:  用GB2312编码的文件,用UTF-8来解码,当然会出现乱码!














 

 

原创粉丝点击