实例解析 C/C++ 疑难问题(一)

  • 内联函数的定义方法
定义 内联函数的方法很简单,只要在函数定义的头前加上关键字inline即可。 内联函数的定义方法与一般函数一样。如:

inline int add_int (int x, int y, int z)
{

return x+y+z;
}


  在程序中,调用其函数时, 该函数在编译时被替代,而不是像一般函数那样是在运行时被调用。

  使用 内联函数应注意的事项

   内联函数具有一般函数的特性,它与一般函数所不同之处公在于函数调用的处理。一般函数进行调用时,要将程序 执行权转到被调用函数中,然后再返回到调用它的函数中;而 内联函数在调用时,是将调用表达式用 内联函数体来替换。在使用 内联函数时,应注意如下几点:
          1.类内定义的函数是内联函数,类外定义的函数是非内联函数(短函数可以定义在类内,长函数可以定义在类外)。

  2.可以为类外定义的函数指定 inline 关键字,强行为内联函数。

  3.在内联函数内不允许用循环语句和开关语句。

  4.内联函数的定义必须出现在内联函数第一次被调用之前。

  2. 内联函数的定义必须出现在 内联函数第一次被调用之前。

  3.本栏目讲到的类结构中所有在类说明内部定义的函数是 内联函数
开关语句:
switch语句下面的case后面的序号是不是数字由小到大执行。就像
case2:
case1:
case0:是不是先执行case0还是由上往下执行。
还有default是不是不管放哪都是最后执行。
第一个个问题是这样的,switch中有值和case后面的值相等的时候,就执行case那行语句,switch都是从上往下判断的,C语言中的语句执行流程就是从下往上(别弄糊涂了),所以switch 执行也是一样的。不是先执行case0,而是先判断switch中的值是否为 2 -> 1 -> 0 从上往下依次判断下来。如果switch里的值为0 的话,就执行case0,好好想下!

第二个问题,说实话我以前没有把default放到case中间编译过(也不知道编译器是否报错),呵呵,虽然这种是无用功,但是对于了解编译还是有帮助的,反正结果应该是这样,执行到default后,下面的case都不会执行。

“default是不是不管放哪都是最后执行”,不是这样的,不管default放到哪儿,顺序由上往下执行到default的时候,它就会执行,尽管你后面还有case语句等等,都忽略了!



1.动态内存的传递:

# include <iostream>

void GetMemory(char *p,int num)

{

p=(char*)malloc(sizeof(char)*num);

}

int main()

char *str=NULL;

GetMemory(str,100);

strcpy(str,"hello");

return 0;

}

总结:指针当参数传进去的是它的一个副本,指针str本身还是NULL,str并不指向指针P指向的那段内存。函数GetMemory没有返回值,故函数退出后,指针副本消失,这样会造成内存泄露。

2.局部数组和全局数组

char c[] ="hello world";

char* c="hello world";

总结:前者可以更改内容,后者不可更改。

前者先在内存的栈区分配数组,然后赋值;后者先在全局区域分配字符串常量的内存,然后将字符串常量的地址赋值给指针c。

3.下列程序会在哪行出现错误:

struct S

{

int i;

int *p

}

void main()

{

S s;

int *p=&s.i;

p[0]=4;

p[1]=3;

s.p=p;

s.p[1]=1;

s.p[0]=2;//会出现错误。

//p[2]=7;//运行到这里也会出错,因为超出结构体的内存范围,对于一个未说明的地址直接访问会出错

总结:程序运行到 s.p[0]=2;会出现错误。首先明白在结构体里面,指针P在i的接下来的4个字节的内存位置。

s.p=p;相当于s.p存了p的值,即&s.i,当执行p[0]=4;p[1]=3;的时候,p的值始终是&s.i。

s.p[1]相当于&s.i+1,即s.p在结构体中的内存位置。

所以 s.p[1]=1将0x00000001写入s.p空间

s.p[0]=2,相当于对一个未作声明的地址直接进行写访问。

4.数组指针与指针数组

int (*a)[10];

a++;

这是定义一个指向10个元素的数组的指针。即数组指针

a++指向第11个元素(如果数组有第11个元素的话)。

数组名本身就是一个指针,再加一个&就是双指针。

#include <iostream> 

#include<stdio.h>

int main()

{

int v[2][10]={ {1,2,3,4,5,6,7,8,9,10},{11,12,13,14,15,16,17,18,19,20}};

int (*a)[10]=v;//数组指针

cout<<**a<<endl;

a++;

cout<<**a<<endl;

return 0;

}

程序输出1, 11。数组指针就是一个二级指针。

 main()

{

  inta[5]={1,2,3,4,5};  

  int *ptr=(int *)(&a+1);

  printf("%d,%d",*(a+1),*(ptr-1));

}

答案:2。5 

*(a+1)就是a[1],*(ptr-1)就是a[4],执行结果是2,5

&a+1不是首地址+1,系统会认为加一个a数组的偏移,是偏移了一个数组的大小(本例是5个int)

int *ptr=(int *)(&a+1);

ptr实际是&(a[5]),也就是a+5

原因如下:&a是数组指针,其类型为 int (*)[5];而指针加1要根据指针类型加上一定的值,不同类型的指针+1之后增加的大小不同,a是长度为5的int数组指针,所以要加 5*sizeof(int),所以ptr实际是a[5],但是prt与(&a+1)类型是不一样的(这点很重要),所以prt-1只会减去sizeof(int*),a,&a的地址是一样的,但意思不一样,a是数组首地址,也就是a[0]的地址,&a是对象(数组)首地址,a+1是数组下一元素的地址,即a[1],&a+1是下一个对象的地址,即a[5].

5.句柄与指针的区别和联系

句柄是一个指向指针的指针,我们知道Windows是一个以虚拟内存为基础的操作系统,在这种系统环境下,内存管理器经常在内存中来回移动对象,以满足各种应用程序的需求。对象被移动,意味着他的地址也跟着变化。如果地址总是变化,我们到哪里去寻找这个对象,为此Windows专门腾出一块内存地址,用来专门登记各应用对象在内存中的地址变化,而这个存储地址本身是不变化的,内存管理器将对象新的地址告诉这个句柄地址来保存。这个地址是对象装载时由系统分配的。

 6. for循环语句

for(表达式1;表达式2;表达式3)语句;
这里边的“语句”就是循环体语句,若其中只有一条语句,可以不用花括号;若多于一条,则必须用花括号将这些循环体语句括起来。

(1)先操作表达式1;
(2)操作表达式2,若其值为真(值为非O),则执行for
语句中的循环体语句一次,然后执行下面第三步,若为假
(值为O),则结束循环,转到第5步;
(3)操作表达式3;
(4)转回上面第2步骤继续执行;
(5)结束循环,执行for语句下面的语句。

7.C++虚析构函数调用问题

例1:

我们知道,用C++开发的时候,用来做基类的类的析构函数一般都是虚函数。可是,为什么要这样做呢?下面用一个小例子来说明:    

  有下面的两个类:

class ClxBase
{
public:
    ClxBase() {};
    
virtual ~ClxBase() {};

    
virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
};
class ClxDerived : public ClxBase
{
public:
    ClxDerived() {};
    
~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; }; 

    
void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };
};

    代码

ClxBase *pTest = new ClxDerived;
pTest
->DoSomething();
delete pTest;

    的输出结果是:

Do something in class ClxDerived!
Output from the destructor of class ClxDerived!

    这个很简单,非常好理解。
    但是,如果把类ClxBase析构函数前的virtual去掉,那输出结果就是下面的样子了:

Do something in class ClxDerived!

    也就是说,类ClxDerived的析构函数根本没有被调用!一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。我想所有的C++程序员都知道这样的危险性。当然,如果在析构函数中做了其他工作的话,那你的所有努力也都是白费力气。
    所以,文章开头的那个问题的答案就是--这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
    当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

  如果继承是多态的方式,则一定要将基类的析构函数设置为virtual形式的。

例2:

#include "stdafx.h"
#include <iostream>
using namespace std;
class Base
{
public:
virtual ~Base(){cout<<"~Base"<<endl;};
};
class Derived:public Base
{
public:
~Derived(){cout<<"~Derived"<<endl;}
};
int _tmain(int argc, _TCHAR* argv[])
{
//char *p="google";
//char pp[]="abcdefg";
//cout<<pp[1];
Base *p=new Derived;
//p->~Base();
delete p;
system("Pause");
return 0;
}

输出:~Derived ~Base,如果去掉virtual 则输出为:~Base;基类析构函数声明为虚函数时,就是动态绑定;否则就是静态绑定。

    不管什么情况下,类的实例都会调用析构函数,没有自定义的,就用默认的,默认的析构函数可以清除类变量,如string之类(自带构造和析构函数的类)的变量,如要清除指向对象的指针,一定要自定义的析构函数。

8.拷贝构造函数(浅拷贝和深拷贝)

   深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

 在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误

#include <iostream>

using namespace std;
class CA
{
 public:
  CA(int b,char* cstr)
  {
   a=b;
   str=new char[b];
   strcpy(str,cstr);
  }
  CA(const CA& C)
  {
   a=C.a;
   str=new char[a]; //深拷贝
   if(str!=0)
    strcpy(str,C.str);
  }
  void Show()
  {
   cout<<str<<endl;
  }
  ~CA()
  {
   delete str;
  }
     private:
      int a;
      char *str;
};

int main()
{
 CA A(10,"Hello!");
 CA B=A;
 B.Show();
 return 0;

深拷贝和浅拷贝的定义可以简单理解成:如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候,这个过程就可以叫做深拷贝,反之对象存在资源,但复制过程并未复制资源的情况视为浅拷贝。

浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错。

   Test(Test &c_t)是自定义的拷贝构造函数,拷贝构造函数的名称必须与类名称一致,函数的形式参数是本类型的一个引用变量,且必须是引用。

当用一个已经初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用,如果你没有自定义拷贝构造函数的时候,系统将会提供给一个默认的拷贝构造函数来完成这个过程,上面代码的复制核心语句就是通过Test(Test &c_t)拷贝构造函数内的p1=c_t.p1;语句完成的。

9.深层揭秘 extern "C"

  实现C++与C及其它语言的混合编程。    

   被extern "C"限定的函数或变量是extern类型的;extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。

 (1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:

extern "C"
{
#include "cExample.h"
}


  而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。

  笔者编写的C++引用C函数例子工程中包含的三个文件的源代码如下:

/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
return x + y;
}
// c++实现文件,调用add:cppFile.cpp

extern "C" 
{
    #include "cExample.h"
}
int main(int argc, char* argv[])
{
add(2,3); 
return 0;
}

如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" { }。

(2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。
  笔者编写的C引用C++函数例子工程中包含的三个文件的源代码如下:

//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
    return x + y;
}
/* C实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 ); 
return 0;
}

10.子类对父类的同名函数的覆盖

如果一个类,存在和父类相同的函数,那么,这个类将会覆盖其父类的方法,除非你在调用的时候,强制转换为父类类型,否则试图对子类和父类做类似重载的调用是不能成功的。

关于函数重定义

class A

{

public:

void fun()

{

printf("A\n");

}

};

class B:public A

{

public:

void fun()

{

printf("B\n");

}

};

int main(int argc, char* argv[])

{

A a;

a.fun();

B b;

b.fun();

b.A::fun();

printf("\n");

return 0;

}

如果基类的函数是 virtual,在派生类里重定义,才会动态绑定,否则就是屏蔽了。这种程序本身就有很大的弊病,作为讨论可以,但真正使用的话,还是抛弃的好。子类尽量不要重新定义继承而来的非虚函数,这会导致“不变性凌驾特异性”的性质(effect C++)混乱。如果要重写,把父类的相应函数定义为虚函数。子类会继承父类的所有成员,在子类中重定义父类的同名函数后,只是在用子类对象调用该函数是只会执行子类重定义后的函数,如果要调用父类的同名函数则要用::域运算符来调用!

父类虚函数,子类重新定义,但前面没有virtual关键字

#include 

using namespace std;

class Parent{
public: 
 void virtual foo(){
  cout << "A" << endl;
 } 
};

class Son:public Parent{
public:
 //形成覆盖,子类重新定义父类的虚函数
 void foo(){
  cout << "foo from son" << endl;
 }
};

int main(){
 Parent *pa = new Parent();
 pa->foo();
 Son* pb = (Son*)pa;
 pb->foo();
 delete pa,pb;
 pa = new Son();
 pa->foo();
 pb = (Son*)pa;
 pb->foo();
 return 0;
}

输出 :AABB

10 .类成员函数的重载、覆盖和隐藏区别?

答案:
a.成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
b.覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。(子类的函数可以没有关键字virtual,也形成对父类函数的覆盖)
c.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)

11.类成员变量和函数的地址

记住:函数名字本身就是一个指针

做下面的一个测试

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值