C++基础知识回顾

一、关键字与其他

1.const的作用

C++ 中的const 是一种类型修饰符,修饰的变量或对象的值是不能被更新的。

与#define宏定义常量的区别:

  1. const常量具有类型,编译器可以进行安全检查;
  2. #define宏定义没有数据类型,只是简单的字符串替换,不能进行安全检查

建议尽量使用const代替#define

(1). const修饰普通类型的变量(全局变量、局部变量)

  • 注意:声明时必须进行初始化(!c++类中则不然),否则会报[Error] uninitialized const ‘a’ [-fpermissive]错误
const int  data= 7; 	//初始化
int  key= data; 	// 正确
data = 8;       // 发生[Error] assignment of read-only variable 'data'

(2). const 修饰指针变量 (对应以下三种情况)

  • 只有一个const,如果const位于*左侧,表示指向常量的指针,不能通过引用修改该数据,但指针本身是变量,可以指向其他的内存单元。例如:
const int *p = 8; //*指针指向的内容 8 不可改变。简称左定值,因为 const 位于 * 号的左边
  • 只有一个const,如果const位于*右侧,表示指向指针常量,即该指针不能指向其他内存地址,但所指的数据可以通过解引用修改,例如:
/**************************************************************
*对于 const 指针 p 其指向的内存地址不能够被改变,但其内容可以改变。
*简称,右定向。因为 const 位于 * 号的右边
*******************************************************************/
int a = 8;
int* const p = &a;
*p = 9; // 正确
int  b = 7;
p = &b; // 错误
  • 两个const,*左右各一个,表示指针和指针所指数据都不能修改。
int a = 8;
const int * const  p = &a;//这时,const p 的指向的内容和指向的内存地址都已固定,不可改变。

(3). const参数传递和函数返回值

  1. 函数返回值,这个跟const修饰普通变量以及指针的含义基本相同
const int func();//这个本身无意义,因为参数返回本身就是赋值给其他的变量!
const int* func();//指针指向的内容不变。
  1. 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改
/****传递过来的参数及指针本身在函数内不可变,无意义!****/

void func(const int data); // 传递过来的参数不可变

void func(int *const data); // 指针本身不可变

void StringCopy(char *sr, const char *src);//参数指针所指内容为常量不可变

void func(const A &a)//参数为引用,为了增加效率同时防止修改。

(4)、const修饰类成员函数

const 修饰类成员函数,其目的是防止成员函数修改被调用对象的值,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为 const 成员函数

class Student
{
private:
    int stduent[100];
public:
    Student(int i); 
    const int number;
};

Student::Student(int i):number(i)
{

}

const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.

2、extern的作用

按照标准解释就是: extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。此外extern也可用来进行链接指定。

通俗来说就是有两个作用:

  1. extern与"C"一起连用时,这就涉及到了一个问题的解决。C++调用C函数,引用C的头文件时,需要加extern “C”.
/*******************************************************************
**	
 1. C++虽然兼容C,但C++文件中函数编译后生成的符号与C语言生成的不同。
 2. 因为C++支持函数重载,C++函数编译后生成的符号带有函数参数类型的信息,而C则没有。
**
*************************************************************************/

void fun(int a,int b);

/*****************************************************************************

 1. void fun(int a, int b)函数经过C++编译器生成.o文件后,fun会变成形如fun@aBc_int_int#%$也可能是别的,这样要看编译器了;
 2. 而C的话则会是形如_fun, 就是说:相同的函数,在C和C++中,编译后生成的符号不同。
 3. 这就导致一个问题:如果C++中使用C语言实现的函数,在编译链接的时候,会出错,提示找不到对应的符号。
 4. 此时extern "C"就起作用了:告诉链接器去寻找_fun这类的C语言符号,而不是经过C++修饰的符号。

**********************************************************************/

  1. 当extern不与"C"在一起修饰变量或函数时,如在头文件中: extern int g_Int; 它的作用就是声明函数或全局变量的作用范围的关键字.

3、virtual

前言: 说道virtual,这就不能不说一下C++的多态了,C++多态是通过虚函数(virtual)来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。

C++多态具体含义可以定义为:

  • 多态的含义是多种状态,意思就是说有一对继承关系的两个类,在基类的函数前加上virtual关键字则表示为虚函数,在派生类中重写该函数(名字、参数、返回值相同,加不加virtual也行),运行时将会根据对象的实际类型来调用相应的函数。
  • 如果对象类型是派生类,就调用派生类的函数;
  • 如果对象类型是基类,就调用基类的函数

c++中的多态性主要体现在C++关键字virtual的两个主要方面上的应用:虚函数与虚基类;

虚函数应用:

  1. 虚函数是指一个类中你希望重载的成员函数,当你用一个基类指针或引用指向一个继承类对象的时候,你调用一个虚函数,实际调用的是继承类的版本。
  2. 虚函数作用主要是实现了多态的机制;
  3. 虚函数最关键的特点是“动态联编”,它可以在运行时判断指针指向的对象,并自动调用相应的函数;
  4. c++规定,当一个成员函数被声明为虚函数后,其派生类的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每层声明该函数时都加上virtual,使程序更加清晰;
class A
{
public:
 void FuncA()
 {
     printf( "FuncA called\n" );
 }
 virtual void FuncB()
 {
     printf( "FuncB called\n" );
 }
};
class B : public A
{
public:
 void FuncA()
 {
     A::FuncA();
     printf( "FuncAB called\n" );
 }
 virtual void FuncB()
 {
     printf( "FuncBB called\n" );
 }
};
void main( void )
{
 B	b;
 A	*pa;
 pa = &b;
 A *pa2 = new A;
 pa->FuncA(); //( 1) 
 pa->FuncB(); //( 2)
 pa2->FuncA(); //(3)
 pa2->FuncB();//(4)
 delete pa2;
}

  • 答案是:(1)FuncA called、(2)FuncBB called、(3)FuncA called、(4)FuncB called

解析

  1. 首先呢,我们来看pa是什么,pa被声明为A类(基类)的指针,然后指向为子类B;
  2. pa->FuncA(),虽然基类A的指针指向了B类,但是A类中的FuncA()并不是虚函数,所以呢程序在运行中会默认认为pa->FuncA()是A的指针指向了A类的FuncA(),而不是执行B类的FuncA()(不知道这样解释对不对);
  3. pa->FuncB(),由于你在基类A的FuncB()声明了virtual虚函数,在执行过程中,这条语句实际调用的是继承类B的FuncB()函数。这就体现了虚函数的多态性了。
  4. pa2就应该很简单了吧,直接声明的是A类

更深入了解虚函数机制

4、sizeof与strlen

  • sizeof 是C/C++中的一个操作符,其作用就是返回一个对象或者类型所占的内存字节数。
  • strlen是一个函数,其作用是计算给定字符串的长度,不包括’\0’在内。函数原型为:
size_t strlen(const char *string);

strlen这个函数倒是好理解、简单使用,针对的是字符串的长度,遇 ‘\0’ 则停止。一般会有三个问题:

(1) 已初始化的数组

	char data[]="123456";
	cout<<strlen(data)<<endl;		//输出结果为6

(2)局部变量未初始化的数组:

	char data1[10];
	cout<<strlen(data1)<<endl;	//为随机值,可能是1,也可能是3,反正就是遇到'\0'就停止

(3) 全局变量未初始化的数组:

char data[10];
int main(){
	cout<<strlen(data)<<endl;	//值为0,因为定义在全局的数组,即使没有初始化,系统也会默认初始化为'\0'
	return 0;

而sizeof这个就复杂了一点,对于操作的是数组来说,需要注意有一下问题:

  1. 无论是整型数组还是字符数组,数组名作为右值的时候都代表数组首元素的首地址;
  2. 数组发生降级(数组名退化为数组首元素的地址)的情况:数组传参、数组名参与运算;
  3. 数组名不会发生降级的情况:sizeof(数组名)、取地址数组名(取到的是整个数组的地址而不是首元素的地址);

具体演示:

(1)第三个问题演示

	char data1[6]={1,2,3,4,5};
	char data[]={1,2,3,4,5}
	cout<<sizeof(data1)<<endl;
	cout<<sizeof(data)<<endl;
	cout<<sizeof(&data)<<endl;
  • 结果分别为6,5,4(我的编译器是64位,结果是8,32位的是4)。这个好理解,data1数组定义为大小为6,尽管这里显示初始化了5个,但最后一个系统会默认初始化为0;

(2) 数组发生降级,数组名参与运算

char data[]={1,2,3,4,5};
cout<<sizeof(data+0)<<endl;
  • 结果为8(64位的),data+0代表的是data数组的首地址;

sizeof对类变量来说,也稍微复杂了点

class CTest
{
    public:
        CTest():m_chData(‘\0),m_nData(0)
        {
        }
        virtual void mem_fun(){}
    private:
        char m_chData;
        int m_nData;
        static char s_chData;
};
char CTest::s_chData=’\0;
  • 若按4字节对齐sizeof(CTest)的值是多少?答:12
  • 若按1字节对齐sizeof(CTest)的值是多少?答:9

那这是怎么算出来的呢?具体解释如下:

  • 在类中,如果什么都没有,则类占用1个字节,一旦类中有其他的占用空间成员,则这1个字节就不在计算之内,如一个类只有一个int则占用4字节而不是5字节。
  • 如果只有成员函数,则还是只占用1个字节,因为类函数不占用空间
  • 首先看看有没有被virtual修饰的量,有就加4。因为虚函数存在一个虚函数表,需要4个字节,数据成员对象如果为指针则为4字节,注意有字节对齐,如果为13字节,则进位到16字节空间。
  • 当类中出现static成员变量的时候,static成员变量是存储在静态区当中的,它是一个共享的量,因此,在为这个类创建一个实例对象的时候,是无需再为static成员变量分配空间的

5、Static

什么是static?

  • static 是 C/C++ 中很常用的修饰符,它被用来控制变量的存储方式和可见性;

使用方式一共有一下几种:

  1. 修饰全局变量,其表现为只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
  2. 修饰局部变量(函数中的静态变量),其表现为不能被其他文件调用,该变量在全局数据区分配内存,首次执行语句,若没有初始化变量默认初始化为0,以后即使多次调用该函数,静态变量的空间也只分配一次,不再初始化。它始终保存在全局数据区内,直到程序结束。相比局部变量存储在栈内,函数运行完后就被释放掉。
void Try_do() 
{ 
    static int numble; //默认初始化为0;
    cout << "该函数调用的第"<< numble+1<<"次 "<<endl;;
    numble+=1; 
} 

int main() 
{ 
    for (int i=0; i<5; i++){
    	Try_do();	 
	} 
   cout<<"End<<endl;      
    return 0; 
} 
  1. 修饰普通函数,其表明该函数的作用范围仅在定义该函数的文件内才能使用,在其他的文件下无法使用;
  2. 修饰类成员;
  3. 修饰类对象;

6、C++之Lambda表达式

Lambda表达式是C++11的一种新特性,用于定义并创建匿名的函数对象,以简化编程工作。

语法形式如下:

  • [函数对象参数] (操作符重载函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}

7、复写strcpy函数

strcpy的原型为char *strcpy(char *dest, const char *src) ,其作用为将scr的字符串赋值到dest里面去。

  • dest – 指向用于存储复制内容的目标数组。
  • src – 要复制的字符串。
#include <assert.h>

char* strcpy(char*dest,const char* scr){
	assert((dest!=NULL&&scr!=NULL));
	char*address=dest;
	while((*dest++=*scr++)!='\0');
	return address;
}

int main(){
	char src[40];
    char dest[100];

   strcpy_1(src, "This is runoob.com");
   strcpy_1(dest, src);
 
   cout<<"The end is:"<<dest<<endl;
   
   return 0;
 }
	
  • 结果输出为:The end is:This is runoob.com

二、C与C++的一些区别

2.1、如何判断一段程序是由C编译程序还是由C++编译程序编译的

  1. 如果是要你的代码在编译时发现编译器类型,就可以通过判断_cplusplus或_STDC_宏,当然许多编译器还有其他编译标志宏。
#ifdef __cplusplus
#define USING_C 0
#else
#define USING_C 1
#endif

if(USING_C)
	printf("C\n");
else 
	printf("C++\n");

  • 如果要判断已经编译的代码的编译类型,就用nm查一下输出函数符号是否和函数名相同,相同为c,不同为c++。原因如下:

  • 由于c语言是没有重载函数的概念的,所以c编译器编译的程序里,所有函数只有函数名对应的入口。而c++语言有重载函数的概念,如果只有函数名对应的入口,则会出现混淆,所以c++编译器编译的程序,是函数名+参数类型列表对应到入口。但主要如果是main函数的话就不能了

2.2、C与C++的struct区别,struct与class的区别

C与C++中的struct区别:

  1. (1)C中的struct是是用户自定义数据类型,没有成员函数,而C++的struct是是用户自定义数据类型,可以有成员函数。
  2. (2)C中的struct成员数据是没有访问权限设定的,而C++的有访问权限;
  3. C中的struct是没有继承关系的,而C++有;

C++的class与struct的区别:

  1. (1)class的成员默认访问权限是private,而struct是public的;
  2. (2)从继承关系上看,class是private的,而struct是piblic的;
  3. (3)struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。

三、C++ 类中的赋值、深拷贝和浅拷贝

需了解: 对一个已知对象进行拷贝时,如果没有自定义拷贝构造函数,那么系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。

那么若没有自定义拷贝构造函数时可能会带来什么危险呢:(假设Type A,Type B都已初始化,若A=B)

  • 将B赋值给A,执行缺省的拷贝构造函数会意味着原A的内存没有被释放,造成内存泄漏;
  • 一般来说,变量A和B声明后都会得到一个栈的内存块,如果对象有位于heap上的域的话,其不会为拷贝对象分配heap上的空间,而只是指向相同的heap上的同一个地址。若A修改成员量的值,则B对应的值也跟着A变
  • 当程序执行完毕后,调用析构函数时会发生两次析构;

注意: 要区分赋值和拷贝构造函数

Type A();
Type B=A;//可以改写为B(A),表示为使用拷贝构造函数
//第二种
Type B;
B=A	//表示为赋值操作

通而易懂的说法来说就是:

  • 当声明对象的同时又进行的初始化操作,则称之为拷贝运算,表现形式为 Type A(Emp x); Type B=A;//B(A),其实际调用的是B(A)这样的浅拷贝操作。
  • 当声明了对象之后,再进行的赋值运算,我们称之为赋值运算。表现形式为Type A(Emp x); Type B;B=A;此时实际调用的类的缺省赋值函数B.operator=(A);

分析:

  • 以上这种B=A,其实就是一种浅拷贝。如果对象中没有其他的资源(如:指针、堆,文件,系统资源等),只是简单赋值或者打印信息,那么也是可以的;
  • 但若存在指针、堆分配、文件等资源时,例如指针:如果直接赋值,当对象快结束时会调用两次析构函数,而导致指针悬挂现象
  • 毕竟浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间;
  • 而深拷贝不但对指针进行拷贝, 而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

例子的话我就不写了,你们自己上手试试,这里我给出某位大佬的测试的代码:

#include <iostream>  
using namespace std;
 
class Student
{
private:
	int num;
	char *name;
public:
	Student();
	~Student();
};
 
Student::Student()
{
	name = new char(20);
	cout << "Student" << endl;
 
}
Student::~Student()
{
	cout << "~Student " << (int)name << endl;
	delete name;
	name = NULL;
}
 
int main()
{
	{// 花括号让s1和s2变成局部对象,方便测试
		Student s1;
		Student s2(s1);// 复制对象
	}
	system("pause");
	return 0;
}
  • Student::Student()这个函数内部里申请了内存分配,当时将s1赋值给s2时,就会出现两次析构;
#include <iostream>  
using namespace std;
 
class Student
{
private:
	int num;
	char *name;
public:
	Student();
	~Student();
	Student(const Student &s);//拷贝构造函数,const防止对象被改变
};
 
Student::Student()
{
	name = new char(20);
	cout << "Student" << endl;
 
}
Student::~Student()
{
	cout << "~Student " << (int)name << endl;
	delete name;
	name = NULL;
}
Student::Student(const Student &s)
{
	name = new char(20);
	memcpy(name, s.name, strlen(s.name));
	cout << "copy Student" << endl;
}
 
int main()
{
	{// 花括号让s1和s2变成局部对象,方便测试
		Student s1;
		Student s2(s1);// 复制对象
	}
	system("pause");
	return 0;
}
  • 而这里呢,自定义了拷贝构造函数,函数内部也另开辟一块新的资源,而不再对拷贝对象中有对其他资源的引用的指针或引用进行单纯的赋值。

题库代码演示:

#include<iostream>
using namespace std;
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()
{
    MyClass obj1(1), obj2(2);
    MyClass obj3 = obj1;//仅仅是打印出数据来,并且如果对象中没有其他的资源(如:堆,文件,系统资源等),则深拷贝和浅拷贝没有什么区别。
    return 0;
}

  • 答案为:1、2、2、4、4、4;

解析:

  • obj1(1),obj2(2)为首次出现的对象,调用了构造函数,即输出结果为1和2;
  • MyClass obj3 = obj1;首先呢声明了obj3这个对象然后直接使用了=运算符,调用obj3的拷贝构造函数MyClass(const MyClass &x)输出2;若改为MyClass obj3 ;obj3=obj1;则输出的是0,3

四、内存管理

在C++中内存管理是非常重要的,因为内存管理常常会面临内存泄漏这一重大问题,这对于开发来说无疑是一个致命因素,因此必须要掌握好这一知识点。总的来说C++的一个内存分成5个区,分别为堆、栈、自由存储区、全局/静态存储区和常量存储区。

栈, 内存分配运算内置于处理器的指令集中,效率高,分配的内存容量有限。通常用于在函数执行时,将函数内部局部变量的储存单元储存在栈上,函数在执行完后会自动释放这些储存单元;

  • 这里引用了力扣题目里的代码给读者思考输出结果。
#include<stdio.h>
char *myString()
{
    char buffer[6] = {0};
    char *s = "Hello World!";
    for (int i = 0; i < sizeof(buffer) - 1; i++)
    {
        buffer[i] = *(s + i);
    }
    return buffer;
}
int main(int argc, char **argv)
{
    printf("%s\n", myString());//输出结果未知
    return 0;
}

堆, 是储存由malloc等分配的内存块,并由free来释放内存;

自由存储区, 是由new分配的内存块,由应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收;

全局/静态存储区, 全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,它们共同占用同一块内存区;

常量存储区, 这是一块比较特殊的存储区,它们里面存放的是常量,不允许修改;

具体内存管理有以下方法使用:

分配释放类型
newdeleteC++表达式
mallocfreeC函数
::operator new()::operator delete()C++函数

3.1、new expression:

用法:

object *p=new object(key1,key2);
...
delete p;

调用底层解析:

new其实就是告诉计算机开辟一段新的空间,但是和一般的声明不同的是,new开辟的空间在堆上,而一般声明的变量存放在栈上。具体实现有两个方面:

  1. 第一方面:其实说跟到底就是调用operator new这个函数,(此函数可以重载达到自己分配内存的目的),然后将指针转换为 object 的对应类型,最后对于有构造函数的类对象会调用其构造函数;如果以上失败抛出bad_alloc异常。(对于VS17下,设定了try函数;但可以通过std::nothrow让new不抛异常)
  2. 第二方面:就是ope new这个函数了,而这个函数又调用了malloc这个函数。
    在这里插入图片描述

删除解析:
编译器解释为先调用析构函数,然后使用operator delete函数释放内存。而operator delete函数的本质又是调用free函数。

续 2.1:Array new

代码用法:

object *p=new object[size];
	...
delete []p;

底层解析:

  • New[]调用其实说白了就是size次调用构造函数,delete[]则就size次调用析构函数。
  • 出现内存泄漏不是发生在new的时候,而是发生在delete时,当delete[]少了[]时候,只是代表调用了一次析构函数。
  • 但当修饰的不是object时,例如int型时,则只需delete就行了。因此来说,特别要注意使用用法。

待续

最近忙其他,这个暂时不补充,万分抱歉,有兴趣学习的可以学《effective C++》,《STL源码解析》,《深度搜索C++对象模型》
书籍的话网上可以去买,PDF的话我可以提供一些给你们
链接:https://pan.baidu.com/s/1NkQM7fokvU7VvPlVhwtnhA
提取码:mncc
先到先得

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值