一些知识点

1 篇文章 0 订阅
1 篇文章 0 订阅

 

1、  简述你对模板方法的设计模式的了解

 

模式定义:

 

        模板方法模式在一个方法中定义了一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类能够在不改变算法结构的情况下,又一次定义算法中的某些步骤。

        模板就是一个方法。更详细的说。这种方法将算法定义成一组步骤。当中的不论什么步骤都能够是抽象的,由子类实现。

这能够确保算法的结果保持不变,同一时候由子类提供部分实现。

 

 

 

 

 

 

 

2、  结构体字节对齐的问题

 

结构体字节对齐的细节和具体编译器实现相关,但一般而言满足三个准则:

     1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;

     2) 结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);

     3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节{trailing padding}。

     对于以上规则的说明如下:

     第一条:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。

     第二条:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员大小的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。

     第三条:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。

      数据结构的成员位置要兼顾成员之间的关系、数据访问效率和空间利用率。顺序安排原则是:四字节的放在最前面,两字节的紧接最后一个四字节成员,一字节紧接最后一个两字节成员,填充字节放在最后。

 

 

 

typedef struct{
    char  a;
    short b;
    char  c;
    int   d;
    char  e[3];
}T_Test;

  首先char a占用1个字节,没问题。

     short b本身占用2个字节,根据上面准则2,需要在b和a之间填充1个字节。

     char c占用1个字节,没问题。

     int d本身占用4个字节,根据准则2,需要在d和c之间填充3个字节。

     char e[3];本身占用3个字节,根据原则3,需要在其后补充1个字节。

     因此,sizeof(T_Test) = 1 + 1 + 2 + 1 + 3 + 4 + 3 + 1 = 16字节。

 

 

 

 

 

 

3、 写代码,用最简单的方法实现复制数组a[100]到b[100];(memcpy)

 

 

#include<iostream>
#include<string.h>
using namespace std;
int main()
{
	char a[100]={'a','b','c','d','e'};
	int b[100];
	cout<<"sizeof is : "<<sizeof(a)<<endl;//计算'\0'
	cout<<"strlen is : "<<strlen(a)<<endl;//不计算'\0'
	//1、如果复制的字节数n超出了dest的空间容量,或者n超出src的容量,这个函数是不会进行判断的,这样就会很危险。需要程序员自己检查是否有溢出的情况出现。
	//2、这个函数不会检查参数dest与参数src所指向的数组(或其他类型)是否具有同样的空间。
	//3、memcpy, memset等函数都是对内存操作的函数,效率很高,当然也很容易出现问题;如果出现src -> dest的大小出现问题,src地址大于dest地址,就会存在dest无法存取完整数据,造成src数据丢下。memcpy本身是有bug的,并没有解决覆盖问题,可以选用memmove代替。
	memcpy(b,a,sizeof(a));
	//不能拷贝string类型,sizeof(string)只是求了固定大小的成员的内存和,而没有求string内部指针指向的存字符的那一段内存
	//如果结构体含有指针,指向某段内存,memcpy的拷贝也会失败
	//对象不能是memcpy简单拷贝,t2 拷贝后,其指针指向所存放的字符串,该字符串所存放地址与t1中是相同的。程序结束的时候,t1,t2分别析构,由于t1和t2指向同一字符串内存,于是该内存释放两次,于是dump。所以,对象的赋值要经过拷贝构造函数定义。
	return 0;
}

4、  代码实现对a[100]的插入排序;

算法实现

#include "stdafx.h"
#include<iostream>
using namespace std;
void InsertSort(int a[], int n)
{
	for (int j = 1; j < n; j++)
	{
		int key = a[j]; //待排序第一个元素
		int i = j - 1;  //代表已经排过序的元素最后一个索引数
		while (i >= 0 && key < a[i])
		{
			//从后向前逐个比较已经排序过数组,如果比它小,则把后者用前者代替,
			//其实说白了就是数组逐个后移动一位,为找到合适的位置时候便于Key的插入
			a[i + 1] = a[i];
			i--;
		}
		a[i + 1] = key;//找到合适的位置了,赋值,在i索引的后面设置key值。
	}
}

 

 

 


5、 代码实现在a[100]里查找的某个值的位置(二分查找法)。必须为有序

 

 

 

 

int binary_research(int arr[],int left,int right,int element)  
{  
    while(left<=right)  
    {     
        int mid = (left+right)/2;  
        if(arr[mid]>element)  
        {  
            right = mid - 1;  
        }  
        else if(arr[mid]<element)  
        {  
            left = mid + 1;  
        }  
        else   
        {  
            return mid;  
        }  
    }  
    return -1;  
}  

 

 

 

 

 

 



6、  定义一天有多少秒(定义long)#define A ***L或const

#define  SEC_PER_YEAR  (111255555555555560UL)
#define  SEC_PER_YEAR  (1UL*356*24*60*60)
#define  SEC_PER_YEAR  (356*24*60*60UL)会出错,因为356*24*60会溢出int类型的长度






5、  完善一个Person类,实现堆栈常用功能,例如出入栈、获得栈顶等,还有实现SetName或GetName等功能。(注意string的使用,构造函数和析构函数,拷贝函数等,更多的还可以重载=运算符)。

 

拷贝构造函数:

classname(constclassname &obj){// 构造函数的主体}

 

  • 通过使用另一个同类型的对象来初始化新创建的对象。

  • 复制对象把它作为参数传递给函数。

  • 复制对象,并从函数返回这个对象。

  • "="赋值符号左边的变量如果是已经声明的,调用了构造函数,便不会调用拷贝构造函数,而是调用重载的"="赋值符号。

  • 调用拷贝构造函数进行初始化的时候,并不会调用“="重载。

 把参数传递给函数有三种方法,一种是值传递,一种是传地址,还有一种是传引用。前者与后两者不同的地方在于:当使用值传递的时候,会在函数里面生成传递参数的一个副本,这个副本的内容是按位从原始参数那里复制过来的,两者的内容是相同的。当原始参数是一个类的对象时,它也会产生一个对象的副本,不过在这里要注意。一般对象产生时都会触发构造函数的执行,但是在产生对象的副本时却不会这样,这时执行的是对象的复制构造函数。为什么会这样?嗯,一般的构造函数都是会完成一些成员属性初始化的工作,在对象传递给某一函数之前,对象的一些属性可能已经被改变了,如果在产生对象副本的时候再执行对象的构造函数,那么这个对象的属性又再恢复到原始状态,这并不是我们想要的。所以在产生对象副本的时候,构造函数不会被执行,被执行的是一个默认的构造函数。当函数执行完毕要返回的时候,对象副本会执行析构函数,如果你的析构函数是空的话,就不会发生什么问题,但一般的析构函数都是要完成一些清理工作,如释放指针所指向的内存空间。这时候问题就可能要出现了。假如你在构造函数里面为一个指针变量分配了内存,在析构函数里面释放分配给这个指针所指向的内存空间,那么在把对象传递给函数至函数结束返回这一过程会发生什么事情呢?首先有一个对象的副本产生了,这个副本也有一个指针,它和原始对象的指针是指向同块内存空间的。函数返回时,对象的析构函数被执行了,即释放了对象副本里面指针所指向的内存空间,但是这个内存空间对原始对象还是有用的啊,就程序本身而言,这是一个严重的错误。然而错误还没结束,当原始对象也被销毁的时候,析构函数再次执行,对同一块系统动态分配的内存空间释放两次是一个未知的操作,将会产生严重的错误。

       上面说的就是我们会遇到的问题。解决问题的方法是什么呢?首先我们想到的是不要以传值的方式来传递参数,我们可以用传地址或传引用。没错,这样的确可以避免上面的情况,而且在允许的情况下,传地址或传引用是最好的方法,但这并不适合所有的情况,有时我们不希望在函数里面的一些操作会影响到函数外部的变量。那要怎么办呢?可以利用复制构造函数来解决这一问题。复制构造函数就是在产生对象副本的时候执行的,我们可以定义自己的复制构造函数。在复制构造函数里面我们申请一个新的内存空间来保存构造函数里面的那个指针所指向的内容。这样在执行对象副本的析构函数时,释放的就是复制构造函数里面所申请的那个内存空间。

       除了将对象传递给函数时会存在以上问题,还有一种情况也会存在以上问题,就是当函数返回对象时,会产生一个临时对象,这个临时对象和对象的副本性质差不多。

拷贝构造函数,经常被称作X(X&),是一种特殊的构造函数,他由编译器调用来完成一些基于同一类的其他对象的构件及初始化。它的唯一的一个参数(对象的引用)是不可变的(因为是const型的)。这个函数经常用在函数调用期间于用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用。 

在C++中,下面三种对象需要拷贝的情况。因此,拷贝构造函数将会被调用。 

1). 一个对象以值传递的方式传入函数体 

2). 一个对象以值传递的方式从函数返回 

3). 一个对象需要通过另外一个对象进行初始化 

以上的情况需要拷贝构造函数的调用。如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。对于第三种情况来说,初始化和赋值的不同含义是构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作赋共同实现的。描述拷贝构造函数和赋值运算符的异同的参考资料有很多。 

拷贝构造函数不可以改变它所引用的对象,其原因如下:当一个对象以传递值的方式传一个函数的时候,拷贝构造函数自动的被调用来生成函数中的对象。如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象这样复制才可以传入它自己的拷贝构造函数,这会导致无限循环。 

除了当对象传入函数的时候被隐式调用以外,拷贝构造函数在对象被函数返回的时候也同样的被调用。换句话说,你从函数返回得到的只是对象的一份拷贝。但是同样的,拷贝构造函数被正确的调用了,你不必担心。 

如果在类中没有显式的声明一个拷贝构造函数,那么,编译器会私下里为你制定一个函数来进行对象之间的位拷贝(bitwise copy)。这个隐含的拷贝构造函数简单的关联了所有的类成员。许多作者都会提及这个默认的拷贝构造函数。注意到这个隐式的拷贝构造函数和显式声明的拷贝构造函数的不同在于对于成员的关联方式。显式声明的拷贝构造函数关联的只是被实例化的类成员的缺省构造函数除非另外一个构造函数在类初始化或者在构造列表的时候被调用。 

拷贝构造函数是程序更加有效率,因为它不用再构造一个对象的时候改变构造函数的参数列表。设计拷贝构造函数是一个良好的风格,即使是编译系统提供的帮助你申请内存默认拷贝构造函数。

 

 

 

事实上,拷贝构造函数是由普通构造函数和赋值操作符共同实现的,

通常的原则是:①对于凡是包含动态分配成员或包含指针成员的类都应该提供拷贝构造函数;②在提供拷贝构造函数的同时,还应该考虑重载"="赋值操作符号。

执行的是浅拷贝,只是将成员的值进行赋值,也即这两个指针指向了堆里的同一个空间,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因

在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间

 

// testSingleMode.cpp : 定义控制台应用程序的入口点。  
//  
#include "stdafx.h"  
#include <iostream>  
  
using namespace std;  
  
class Complex  
{  
private:  
    double m_real;  
    double m_imag;  
  
public:  
    Complex(void){  
        m_real = 0.0;  
        m_imag = 0.0;  
    }  
  
    Complex(double real, double imag){  
        m_real = real;  
        m_imag = imag;  
    }  
  
    Complex(const Complex & c){ //这里就是最经典的拷贝构造函数了  
        m_real = c.m_real;  
        m_imag = c.m_imag;  
    }  
  
    Complex &operator = (const Complex &rhs){   //这里就是最经典的operator=操作符重载了  
        if (this == &rhs){  
            return *this;  
        }  
  
        this->m_real = rhs.m_real;  
        this->m_imag = rhs.m_imag;  
  
        return *this;  
    }  
  
    explicit Complex::Complex(double r){    //explicit的用法,只适用于1个参数的情况  
        m_real = r;  
        m_imag = 0.0;  
    }  
  
};  
  
  
int main()  
{  
    Complex c1, c2; //调用 第15行 默认无参数的构造函数  
    Complex c3(1.0, 2.5);   //调用 第20行 具有2个形参的构造函数  
    //Complex c3 = Complex(1.0, 2.5);   //和上一行是一个意思,所以这个注释了  
    c1 = c3;    //调用 第30行 重载operator=运算符  
    c2 = c3;    //调用 第30行 重载operator=运算符  
    //c2 = 5.2; //隐式转换,需要去掉41行的explicit关键字,才可编译通过  
  
    Complex c5(c2);     //调用 第25行 拷贝构造函数  
    Complex c4 = c2;    //调用 第25行 拷贝构造函数  
  
    getchar();  
    return 0;  
}  


构造函数和析构函数:

 

构造函数不可以是虚函数的,这个很显然,毕竟虚函数都对应一个虚函数表,虚函数表是存在对象内存空间的,如果构造函数是虚的,就需要一个虚函数表来调用,但是类还没实例化没有内存空间就没有虚函数表,这根本就是个死循环。

 

 

派生类对象构造的时候先调用基类的构造函数再调用派生类的构造函数,析构的时候先调用派生类析构函数再调用基类析构函数。

其实这个很好理解,派生类的成员由两部分组成,一部分是从基类那里继承而来,一部分是自己定义的。那么在实例化对象的时候,首先利用基类构造函数去初始化从基类继承而来的成员,再用派生类构造函数初始化自己定义的部分。

同时,不止构造函数派生类只负责自己的那部分,析构函数也是,所以派生类的析构函数会只析构自己的那部分,这时候如果基类的析构函数不是虚函数,则不能调用基类的析构函数析构从基类继承来的那部分成员,所以就会出现只删一半的现象,造成内存泄漏。

所以析构函数要定义成虚函数。

 

 

 

 

 

简单的栈操作

#ifndef CSTOCK_H_  
#define CSTOCK_H_  
  
const int STOCK_SIZE = 100;//定义栈的大小  
typedef int elemType;//定义栈元素类型,目前仅用int来练手  
  
class CStock  
{  
public:  
    CStock(); //构造函数,构造空栈;  
  
    bool push(elemType x); //进栈操作;  
    bool pop(elemType &x); //出栈操作;  
    void clear(); //清空栈;  
    bool isEmpty(); //判断是否栈空;  
    bool isFull(); //判断是否栈满;  
    void print(); //打印栈内元素;  
  
    ~CStock();  
  
private:  
   elemType elem[STOCK_SIZE];  
   int top;//指向栈顶;  
};  
  
#endif  
#include "Stock.h"  
#include<iostream>  
using std::cout;  
using std::endl;  
  
//构造函数  
CStock::CStock():top(-1)  
{  
  
}  
  
//进栈操作,进栈成功返回true,否则返回false;  
bool CStock::push(elemType x)  
{  
   if(top == STOCK_SIZE - 1)  
   {  
       return false;  
   }  
   elem[++top] = x;  
   return true;  
}  
  
//出栈操作,由形参x将元素带出主调函数,出栈成功返回true,否则返回false;  
bool CStock::pop(elemType &x)  
{  
   if(top == -1)  
   {  
       return false;  
   }  
   x = elem[top--];  
   return true;  
}  
  
//清空栈,使栈为空;  
void CStock::clear()  
{  
    top = -1;  
}  
  
//判断栈是否为空  
bool CStock::isEmpty()  
{  
    return top == -1;  
}  
  
//判断栈是否栈满  
bool CStock::isFull()  
{  
    return top == STOCK_SIZE - 1;  
}  
  
//打印栈  
void CStock::print()  
{  
   for(int i = 0; i <= top; i++)  
   {  
       cout << elem[i] << "\t";  
  
       if( (i+1) % 5 == 0)  
           cout << endl;  
   }  
}  
//析构函数  
CStock::~CStock(void)  
{  
  
}  

 单例模式加锁例子

class singleton
{
protected:
    singleton()
    {
        pthread_mutex_init(&mutex);
    }
private:
    static singleton* p;
public:
    static pthread_mutex_t mutex;
    static singleton* initance();
};

pthread_mutex_t singleton::mutex;
singleton* singleton::p = NULL;
singleton* singleton::initance()
{
    pthread_mutex_lock(&mutex);
    if (p == NULL)
        p = new singleton();
    pthread_mutex_unlock(&mutex);
    return p;
}

 

int a[2] = {1,2};  
int main(){  
        printf("a = %p\n", a); // I  
        printf("&a = %p\n", &a); // II  
        printf("a + 1 = %p\n", a + 1);// III  
        printf("&a + 1 = %p\n", &a + 1);// IV  
  
        return 0;  
}  
a = 0x804a014
&a = 0x804a014
a + 1 = 0x804a018
&a + 1 = 0x804a01c

 

a和&a有什么区别

在C中, 在几乎所有使用数组的表达式中,数组名的值是个指针常量,也就是数组第一个元素的地址。 它的类型取决于数组元素的类型: 如果它们是int类型,那么数组名的类型就是“指向int的常量指针“。


对于II 和 IV 则是特殊情况,在《C和指针》p142中说到,在以下两中场合下,数组名并不是用指针常量来表示,就是当数组名作为sizeof操作符和单目操作符&的操作数时。 sizeof返回整个数组的长度,而不是指向数组的指针的长度。 取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量的指针。
所以&a后返回的指针便是指向数组的指针,跟a(一个指向a[0]的指针)在指针的类型上是有区别的。

 

以字符串形式出现的,编译器都会为该字符串自动添加一个0作为结束符,如在代码中写
  "abc",那么编译器帮你存储的是"abc\0"

 

char *str1 = "asdfgh";//双引号做了1:申请了空间(常量区)存放字符串,2:在字符串后面加上'\0',返回地址,str1存放的地址是变量。
char str2[] = "asdfgh";//因为定义的是一个字符数组,所以相当于定义了一些空间来存放"asdfgh",编译器把这个语句解析为char str2[] ={'a','s','d','f','g','h','\0'}。但char str2[7],str2="asdfgh"则错误,"asdfgh"返回地址,而str2存放的地址是常量(指向数组的首元素地址),不能给常量赋值,并且因为定义的是一个普通字符指针,并没有定义空间存放"asdfgh",所以编译器会帮我们找地方存放字符串,会把"asdfgh"当成常量并放在程序的常量区
char str3[8] = {'a', 's', 'd'};
char str4[] = "as\0df";
执行结果是:
sizeof(str1) = 4;  strlen(str1) = 6;
sizeof(str2) = 7;  strlen(str2) = 6;
sizeof(str3) = 8;  strlen(str3) = 3;
sizeof(str4) = 6;  strlen(str4) = 2;

str1是字符指针变量,sizeof获得的是该指针所占的地址空间,32位操作系统对应4字节,所以结果是4;strlen返回的是该字符串的长度,遇到'\0'结束,'\0'本身不计算在内,故结果是6。

str2是字符数组,大小由字符串常量"asdfgh"确定,sizeof获得该数组所占内存空间大小,包括字符串结尾的'\0',所以结果为7;strlen同理返回6。

str3也是字符数组,但大小确定为8,故sizeof得到的结果是8;strlen统计'\0'之前所有字符的个数,即为3;

str4是常量字符数组,sizeof得到字符总数即6;strlen计算至'\0'结束,因此返回2;

当数组名作为sizeof操作符和单目操作符&的操作数时。 sizeof返回整个数组的长度。

总结一句就是:sizeof计算的是变量的大小,包括'\0'在内,而strlen计算的是字符串的长度,以'\0'作为长度结束判断,不计算'\0'。

 

数组可以用sizeof来得到它的大小,但是如果是指针,就不能使用sizeof来得到大小。因为数组再定义时,已经指明了它的大小,所以编译器可以知道它的实际大小,而对一个指针来说,由于是动态分配的内存,编译器怎么知道它的大小呢?所以不能对指针用sizeof来得到分配的内存大小,此时用sizeof得到的仅仅是指针占用的内存字节数。对Win32程序来说,指针占用4个字节。

 

 

Tcp通信方式:

为什么需要四次断开:

TCP支持双工通信,即收、发两个方向各自独立工作,一个FIN+ACK只能断开一个方向的通信,当要断开两个方向的通信时就需要两个FIN+ACK,即四次操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值