C++ 常见问题

C和C++语言基础

对于带空参数列表的函数,C和C++有很大的不同.在C语言中,声明
int func();
表示"一个可带任意参数(任意数目,任意类型)的函数".这就妨碍了类型检查.而在C++语言中它就意味着"不带参数的函数".

extern 关键词作用

  1. extern声明变量在外部定义?extern修饰函数?
    extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。extern声明不是定义,即不分配存储空间。也就是说,在一个文件中定义了变量和函数, 在其他文件中要使用它们, 可以有两种方式:使用头文件,然后声明它们,然后其他文件去包含头文件;在其他文件中直接 extern。

  2. extern C的作用?用法?

    extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。

    这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。

看下面的一个面试题:为什么标准头文件都有类似的结构?

#ifndef __INCvxWorksh /*防止该头文件被重复引用*/
#define __INCvxWorksh
#ifdef __cplusplus             //告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
extern "C"{
#endif
/*…*/
#ifdef __cplusplus
}
#endif
#endif /*end of __INCvxWorksh*/

分析:

显然,头文件中编译宏"#ifndef __INCvxWorksh 、#define __INCvxWorksh、#endif"(即上面代码中的蓝色部分)的作用是为了防止该头文件被重复引用
那么

#ifdef __cplusplus //(其中__cplusplus是cpp中自定义的一个宏!!!)
extern "C"{
#endif
#ifdef __cplusplus
}
#endif

的作用是什么呢?被extern "C"修饰的变量和函数是按照C语言方式进行编译和链接的.

记住,下面的语句:

extern int a; 仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出错。

通常来说,在模块的头文件中对本模块提供给其他模块引用的函数和全局变量以关键字extern生命。例如,如果模块B要引用模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但并不会报错;它会在链接阶段从模块A编译生成的目标代码中找到该函数。

extern对应的关键字是static,static表明变量或者函数只能在本模块中使用,因此,被static修饰的变量或者函数不可能被extern C修饰。

被extern "C"修饰的变量和函数是按照C语言方式进行编译和链接的:这点很重要!!!!

上面也提到过,由于C++支持函数重载,而C语言不支持,因此函数被C++编译后在符号库中的名字是与C语言不同的;C++编译后的函数需要加上参数的类型才能唯一标定重载后的函数,而加上extern "C"后,是为了向编译器指明这段代码按照C语言的方式进行编译

在本节中我们只看到为C 语言提供的链接指示extern “C”,extern "C"是惟一被保证由所有C++实现都支持的,每个编译器实现都可以为其环境下常用的语言提供其他链接指示例如extern "Ada"可以用来声明是用Ada 语言写的函数,extern "FORTRAN"用来声明是用FORTRAN 语言写的函数,等等因为其他的链接指示随着具体实现的不同而不同所以建议读者查看编译器的用户指南以获得其他链接指示符的进一步信息。

总结 extern “C”

extern “C” 不但具有传统的声明外部变量的功能,还具有告知C++链接器使用C函数规范来链接的功能。 还具有告知C++编译器使用C规范来命名的功能。

static关键字

三种作用 :隐藏变量,静态全局变量,默认初始化为0

  1. 隐藏变量
    当两个文件中存在全局变量时,通过extern关键字可以引用不同文件中的变量。如果加入static关键字,全局变量的作用域在文件内,其他文件无法访问。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏.当两个文件中存在全局变量时,通过extern关键字可以引用不同文件中的变量。如果加入static关键字,全局变量的作用域在文件内,其他文件无法访问。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏.

  2. 静态全局变量

    静态全局变量具有全局作用域,如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。

    静态全局变量在静态存储区分配空间,在程序刚开始运行时就完成初始化,也是唯一的一次初始化

    全局变量具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。

    局部变量也只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。

    静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结 束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。

    静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。

    从分配内存空间看:
    全局变量,静态局部变量,静态全局变量都在静态存储区分配空间,而局部变量在栈里分配空间。

    从以上分析可以看出, 把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。

  3. 默认初始化为0

    全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,

    最后对static的三条作用做一句话总结。首先static的最主要功能是隐藏,其次因为static变量存放在静态存储区,所以它具备持久性和默认值0.

不同修饰对象:普通成员、成员变量、成员函数

  1. 修饰普通函数

static修饰一个函数,则这个函数只能在本文件中调用,不能被同一程序其他文件调用。则其他文件可以定义相同名字的函数,不会发生冲突。

  1. 修饰成员变量

静态数据成员是类的成员,而不是对象的成员,在类中声明static变量或者函数时,初始化时使用作用域运算符来标明它所属类,

(1)静态数据成员可以实现多个对象之间的数据共享,它是类的所有对象的共享成员,它在内存中只占一份空间,如果改变它的值,则各对象中这个数据成员的值都被改变。
(2)静态数据成员是静态存储的,是在程序开始运行时被分配空间,到程序结束之后才释放,只要类中指定了静态数据成员,即使不定义对象,也会为静态数据成员分配空间。非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过类的对象(实例)去访问。

(3)静态数据成员可以被初始化,但是只能在类体外进行初始化,若未对静态数据成员赋初值,则编译器会自动为其初始化为0

 静态成员初始化与一般数据成员初始化不同:
   初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆;
   初始化时不加该成员的访问权限控制符private,public等;

初始化时使用作用域运算符来标明它所属类;
   所以我们得出静态数据成员初始化的格式:
<数据类型><类名>::<静态数据成员名>=<值>

(4)静态数据成员既可以通过对象名引用,也可以通过类名引用。

  1. 修饰成员函数

(1)类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致了它仅能访问类的静态数据成员和静态成员函数。

(2)不能将静态成员函数定义为虚函数:

虚数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable.
对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual.
虚函数的调用关系:this -> vptr -> vtable ->virtual function
(3)由于静态成员声明于类中,操作于其外,所以对其取地址操作,就多少有些特殊,变量地址是指向其数据类型的指针,函数地址类型是一个“nonmember函数指针”。
(4)由于静态成员函数没有this指针,所以就差不多等同于nonmember函数,结果就产生了一个意想不到的好处:成为一个callback函数,使得我们得以将C++和C-based X Window系统结合,同时也成功的应用于线程函数身上。
(5)static并没有增加程序的时空开销,相反它还缩短了子类对父类静态成员的访问时间,节省了子类的内存空间。
(9)为了防止父类的影响,可以在子类定义一个与父类相同的静态变量,以屏蔽父类的影响。这里有一点需要注意:我们说静态成员为父类和子类共享,但我们又重复定义了静态成员,这会不会引起错误呢?不会,我们的编译器采用了一种绝妙的手法:name-mangling 用以生成唯一的标志

const的作用

只要一个变量前面用const来修饰,就意味着该变量里的数据可以被访问,不能被修改。也就是说const意味着“只读”
规则:const离谁近,谁就不能被修改;
const修饰一个变量,一定要给这个变量初始化值,若不初始化,后面就无法初始化。
const修饰全局变量;

const修饰局部变量;

const修饰指针,const int *p1或int const *p1;p1指向的数据不能被修改,但p1本身的值可以被修改(指向其他数据)const修饰指针指向的对象, int * const p2;p2本身的值不能被修改,但它指向的数据可以被修改。(const int *const p3;指针和所指向数据都是常量)const修饰引用做形参;

const 通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制。
在C语言标准库中,有很多函数的形参都被 const 限制了,下面是部分函数的原型:

size_t strlen ( const char * str );
int strcmp ( const char * str1, const char * str2 );
char * strcat ( char * destination, const char * source );
char * strcpy ( char * destination, const char * source );
int system (const char* command);
int puts ( const char * str );
int printf ( const char * format, … );
const修饰成员变量,必须在构造函数列表中初始化; const修饰成员函数,常量成员函数,说明该函数不应该修改非静态成员,但是这并不是十分可靠的,指针所指的非成员对象值可能会被改变。
const成员函数可以被const或非const对象调用,但const对象只能调用const成员函数对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”,只能作为右值使用。

volatile关键字

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

volatile int iNum = 10;

volatile 指出 iNum 是随时可能发生变化的,每次使用它的时候必须从原始内存地址中去读取,因而编译器生成的汇编代码会重新从iNum的原始内存地址中去读取数据。而不是只要编译器发现iNum的值没有发生变化,就只读取一次数据,并放入寄存器中,下次直接从寄存器中去取值(优化做法),而是重新从内存中去读取(不再优化).

多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。一般说来,volatile用在如下的几个地方:

  1. 中断服务程序中修改的供其它程序检测的变量需要加volatile;
  2. 多任务环境下各任务间共享的标志应该加volatile;
  3. 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;

访问寄存器和访问内存谁快

访问寄存器要比访问内存要块,因此CPU会优先访问该数据在寄存器中的存储结果,但是内存中的数据可能已经发生了改变,而寄存器中还保留着原来的结果。为了避免这种情况的发生将该变量声明为volatile,告诉CPU每次都从内存去读取数据。

一个参数可以即是const又是volatile的吗?

可以,一个例子是只读状态寄存器,是volatile是因为它可能被意想不到的被改变,是const告诉程序不应该试图去修改他。

new 和 malloc 的区别

总体概述

分点描述

  1. new分配内存按照数据类型进行分配,malloc分配内存按照大小分配;
  2. new不仅分配一段内存,而且会调用构造函数,但是malloc则不会。new的实现原理?但是还需要注意的是,之前看到过一个题说int* p = new int与int* p = new int()的区别,因为int属于C++内置对象,不会默认初始化,必须显示调用默认构造函数,但是对于自定义对象都会默认调用构造函数初始化。翻阅资料后,在C++11中两者没有区别了,自己测试的结构也都是为0;
  3. new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化;
  4. new是一个操作符可以重载,malloc是一个库函数;
  5. new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会;
  6. malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作;realloc是从堆上分配内存的.当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平;如果数据后面的字节不够,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上.这句话传递的一个重要的信息就是数据可能被移动.
  7. new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。因此对于new,正确的姿势是采用try…catch语法,而malloc则应该判断指针的返回值。为了兼容很多c程序员的习惯,C++也可以采用new nothrow的方法禁止抛出异常而返回NULL;
  8. new和new[]的区别,new[]一次分配所有内存,多次调用构造函数,分别搭配使用delete和delete[],同理,delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n;
  9. 如果不够可以继续谈new和malloc的实现,空闲链表,分配方法(首次适配原则,最佳适配原则,最差适配原则,快速适配原则)。delete和free的实现原理,free为什么知道销毁多大的空间?

实例

int len =7;  
int * a = (int *) malloc (sizeof(int) * len);  
len++;  
int * aold = a;            //重新分配前保存a的地址  这个是多余的  
a = (int *)realloc(sizeof(int)* len);   //重新分配28+4 = 32字节内存给数组a  

前面两句定义了1个长度为7的int 类型数组, 每个元素的字节长度是4, 所以共占28byte 内存.
第3句长度变量+1
第4句 分两种情况:

  1. 假如数组a 内存里接着的4个字节还没被其他对象或程序占用, 那么就直接把后面4个字节加给数组a, 数组前面7个旧的元素的值不变, 数组a的头部地址也不变.

  2. 假如数组 a内存里接着的4个字节已经被占用了, 那么realloc 函数会在内存其他地方找1个连续的32byte 内存空间, 并且把数组a的7个旧元素的值搬过去, 所以数组a的7个旧元素的值也不变, 但是数组a的头部地址变化了. 但是这时我們无需手动把旧的内存空间释放. 因为realloc 函数改变地址后会自动释放旧的内存, 再手动释放程序就会出错了

指针和引用的区别

1)引用是直接访问,指针是间接访问。

2)引用是变量的别名,本身不单独分配自己的内存空间,而指针有自己的内存空间

3)引用绑定内存空间(必须赋初值),是一个变量别名不能更改绑定,可以改变对象的值。

总的来说:引用既具有指针的效率,又具有变量使用的方便性和直观性

关于静态内存分配和动态内存分配的区别及过程

1) 静态内存分配是在编译时完成的,不占用CPU资源;动态分配内存运行时完成,分配与释放需要占用CPU资源;

2)静态内存分配是在栈上分配的,动态内存是堆上分配的;

3)动态内存分配需要指针或引用数据类型的支持,而静态内存分配不需要;

4)静态内存分配是按计划分配,在编译前确定内存块的大小,动态内存分配运行时按需分配。

5)静态分配内存是把内存的控制权交给了编译器,动态内存把内存的控制权交给了程序员;

6)静态分配内存的运行效率要比动态分配内存的效率要高,因为动态内存分配与释放需要额外的开销;动态内存管理水平严重依赖于程序员的水平,处理不当容易造成内存泄漏。

宏定义求两个元素的最小值

#define MIN(A,B) ((A) <= (B) ? (A) : (B))

分别设置和清除一个整数的第三位?

 #define BIT3 (0x1<<3)
 static int a;
 void set_bit3(void)
 { 
 a |= BIT3;
 }
 void clear_bit3(void){
 a &= ~BIT3;
 } 

死循环最佳方式

while(1){}

用变量a给出下面的定义

一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数
int (*a[10])(int);

字符串函数实现

memcpy 实现

void *memcpy(void *dest, const void *src, size_t count) 
{
char *tmp = dest;
const char *s = src;
while(count--){
*tmp++ = *s++;
}
}

strcpy 实现

char *strcpy(char *dst,const char *src) { 
      assert(dst != NULL && src != NULL); 
      char *ret = dst; 
      while((* dst++ = * src++) != '\0') ; 
      return ret; 
}

strcat 实现

char *strcat(char *strDes, const char *strSrc){
	assert((strDes != NULL) && (strSrc != NULL));
	char *address = strDes;
	while (*strDes != ‘\0){
	++ strDes;
	}
	while ((*strDes ++ = *strSrc ++) != ‘\0)
	return address;
}

strcat 实现

char *strncat(char *strDes, const char *strSrc, int count){
	assert((strDes != NULL) && (strSrc != NULL));
	char *address = strDes;
	while (*strDes != ‘\0){
	++ strDes;
	}
	while ((*strDes ++ = *strSrc ++) != ‘\0&& (count-- ));
	*strDes = ‘\0;
	return address;
}

strcmp 实现

int strcmp(const char *str1,const char *str2){
    /*不可用while(*str1++==*str2++)来比较,当不相等时仍会执行一次++,
    return返回的比较值实际上是下一个字符。应将++放到循环体中进行。*/
    while(*str1 == *str2){
        assert((str1 != NULL) && (str2 != NULL));
        if(*str1 == '\0')
            return 0;
        ++str1;
        ++str2;
    }
    return *str1 - *str2;
}

strncmp实现

int strncmp(const char *s, const char *t, int count){
    assert((s != NULL) && (t != NULL));
    while (*s && *t && *s == *t && count –) {
        ++ s;
        ++ t;
    }
    return (*s – *t);
}

strlen实现

int strlen(const char *str){
    assert(str != NULL);
    int len = 0;
    while (*str ++ != ‘\0)
        ++ len;
    return len;
}

string类实现

class String{
public:
//普通构造函数
  String(const char *str = NULL);
//拷贝构造函数
  String(const String &other);
//赋值函数
  String & operator=(String &other) ;
//析构函数
  ~String(void);
private:
  char* m_str;
};

//分别实现以上四个函数
//普通构造函数

String::String(const char* str){
    if(str==NULL) //如果str为NULL,存空字符串{
        m_str = new char[1]; //分配一个字节
        *m_str = ‘\0; //赋一个’\0′
}else{
       str = new char[strlen(str) + 1];//分配空间容纳str内容
        strcpy(m_str, str); //复制str到私有成员m_str中
    }
}

 

//析构函数
String::~String(){
    if(m_str!=NULL) //如果m_str不为NULL,释放堆内存{
        delete [] m_str;
        m_str = NULL;
}
}

 

//拷贝构造函数
String::String(const String &other){
    m_str = new char[strlen(other.m_str)+1]; //分配空间容纳str内容
    strcpy(m_str, other.m_str); //复制other.m_str到私有成员m_str中 
}

 

//赋值函数
String & String::operator=(String &other){
    if(this == &other) //若对象与other是同一个对象,直接返回本{
        return *this
}
    delete [] m_str; //否则,先释放当前对象堆内存
    m_str = new char[strlen(other.m_str)+1]; //分配空间容纳str内容
    strcpy(m_str, other.m_str); //复制other.m_str到私有成员m_str中
    return *this;
}

用struct关键字与class关键定义类以及继承的区别

(1)定义类差别

struct关键字也可以实现类,用class和struct关键字定义类的唯一差别在于默认访问级别:默认情况下,struct成员的访问级别为public,而class成员的为private。语法使用也相同,直接将class改为struct即可。

(2)继承差别

使用class保留字的派生类默认具有private继承,而用struct保留字定义的类某人具有public继承。其它则没有任何区别。

主要点就两个:默认的访问级别和默认的继承级别 class都是private

未完待续~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值