5 лучших концепций программирования на C для разработчиков

5 лучших концепций программирования на C для разработчиков Программирование и разработка

В связи с принятием C в качестве основного языка разработки беспилотных автомобилей, популярность и спрос на C возрождаются. Многие ветераны отрасли изучали программирование с помощью C, но не использовали его годами.

Сегодня мы поможем вам освежить ваши знания C или вывести ваше обучение на новый уровень с помощью 5 наиболее важных передовых концепций C для разработчиков.

1. Распределение динамической памяти

Определение

В C есть два типа распределения памяти: статическое и динамическое. Статическое распределение является более простым из двух и выделяется стеку при выполнении.

После выделения статическое выделение имеет фиксированный размер. Статическое распределение используется для любых глобальных переменных, переменных области файлов и переменных с staticключевым словом.

Распределение динамической памяти является более продвинутым из двух, размер которого может изменяться после выделения. Эта память хранится в куче. В отличие от стека, память кучи не имеет ограничений на переменный размер.

Динамическое распределение выделяет больше памяти по мере необходимости, а это означает, что у вас никогда не выделяется больше памяти, чем вам нужно.

Динамическое выделение памяти также имеет недостатки в производительности. Кэшированные данные кучи не являются смежными, как кэшированные данные стека. В результате получение данных из кучи часто менее эффективно, чем получение данных из стека.

Динамическое выделение памяти также требует больших накладных расходов. Переменные, кэшированные в кучу, должны иметь связанный указатель равного размера, чтобы найти переменную позже.

Например, 4-байтовая переменная также будет иметь связанный 4-байтовый указатель. Это означает, что стоимость ресурсов данных кучи примерно вдвое превышает стоимость ресурсов данных стека.

Хотя динамическая память может масштабироваться, чтобы учесть больше данных, она не освобождает память автоматически при уменьшении объема данных. Вместо этого вы должны вручную освободить память с помощью free()функции.

Вы быстро обнаружите ошибку утечки памяти, если забудете освободить память, что может замедлить или даже привести к сбою вашей программы!

Использует

Накладные расходы динамической памяти лучше всего использовать для переменных выбора, таких как масштабирование структур данных. Лучше по-прежнему использовать статическое распределение для большинства переменных, чтобы избежать замедления вашей программы.

Вам следует использовать динамическую память, когда:

  • Вы не знаете заранее, сколько памяти потребуется структуре
  • Вам нужна структура данных, которая может бесконечно масштабироваться в соответствии с вводом
  • Вы хотите убедиться, что вы никогда не перераспределяете память заранее (автоматическое масштабирование вверх)
  • Вы используете связанный список или объект структуры

Выполнение

#include <stdio.h>
#include <stdlib.h>
int main( )
{
    // Declare pointer variables
    int  *i ;
    float  *a ;
    /* Allocate memory using malloc and store the starting
    address of allocated memory in pointer variable*/
    i = (int*) malloc (sizeof(int));
    a = (float*) malloc (sizeof(float));
    // Declare employee structure
    struct emp
    {
        char name [20];
        int age;
        float sal;
    };
    // Declare structure pointer
    struct emp *e;
    e = (struct emp *) malloc (sizeof (struct emp));
}

В строках 6,7 и 20, i, aи eсоздаются на стеке. Все три указатели. Фрагменты памяти, на которые они указывают, создаются в куче.

В строках 10-11 мы используем malloc( )библиотечную функцию для реализации динамического распределения памяти. При динамическом распределении памяти оба решения — сколько памяти выделить и где выделить — принимаются во время выполнения.

Количество байтов, которые должны быть выделены, передается malloc( )в качестве аргумента. malloc( )выделяет такой объем памяти и возвращает базовый адрес блока памяти, выделенного как недействительный указатель.

Указатель void, возвращаемый malloc( )функцией, соответствующим образом типизируется (преобразуется) в целочисленный указатель, указатель с плавающей запятой или указатель на структуру emp с использованием синтаксиса (целевого типа).

2. Отладка с помощью gdb

Определение

Linux — наиболее часто используемая ОС для программирования на C. В Linux есть инструмент командной строки для отладки, называемый gdb, который поможет вам отладить вашу программу. После установки вы можете запустить всю свою программу с помощью gdb, и он укажет как на логические, так и на синтаксические ошибки.

Это позволяет:

  • Выполнять программу, по одной инструкции за раз
  • Остановить выполнение в любой заданной строке / функции программы
  • Вывести промежуточные значения
  • Понять подробный поток выполнения

Использует

Наиболее важные команды в GDB являются контрольные точки, step, next, и печать значение переменной.

Точки останова приостанавливают выполнение программы в назначенное время, чтобы вы могли оценить состояние программы и значения переменных. Этот инструмент используется для разбиения больших участков кода на более мелкие. Затем вы можете запустить эти блоки, чтобы определить общее местонахождение ошибки.

Step позволяет запускать код по одной строке за раз. Эта команда выполняет следующую строку кода, а затем делает паузу для дальнейших инструкций. Вы будете использовать это, как только сузите ошибку до меньшего фрагмента кода, но не конкретной строки. По мере прохождения чанка вы в конечном итоге обнаружите, какая конкретная строка вызывает ошибку.

Читайте также:  HTML или CSS: в чем разница между ними?

Next похож на, stepно вместо этого выполняет следующую функцию, а не следующую строку. Лучше всего использовать это в тандеме с точками останова для отладки перегруженных функциями или ветвящихся программ. Вы можете запустить каждую модульную функцию и убедиться, что каждая работает должным образом.

Печать значения переменной, используемая после точки останова, stepили nextкоманды для печати текущего значения переменной. Без gdb вам пришлось бы вручную записывать команды печати в вашу программу после каждого взаимодействия с переменной. Вместо этого gdb позволяет уберечь вашу программу от загромождения команд печати, но при этом позволяет отслеживать значение переменной через сегмент программы.

Выполнение

Установить:

apt-get -y install gdb

Скомпилируйте программу с помощью компилятора gdb:

gcc main.-g

Войдите в режим gdb:

gdb a.out

Смотрите исходный код:

list

Создайте точку останова:

gdb) break 6
gdb) run

Шаг одна строка:

gdb) step

Шаг первый:

gdb) next

Проверить текущее значение переменной:

print {variable name}

Нажатие enterв GDB возвращает предыдущую команду. Используйте это, чтобы быстро переходить по блокам кода, не вводя каждый раз заново команды stepили next.

3. Указатели функций

Определение

Указатели на функции — это еще один способ вызвать созданную функцию. Вызов стандартной функции является именем функции и скобками, function1(). Указатели функций позволяют вызывать функцию с адресом памяти функции (*f)().

Для этого вы должны сначала сохранить местоположение нужной функции в переменной-указателе. Как только вы это сделаете, вы можете использовать указатель функции в любом месте, где вы бы использовали стандартный вызов функции.

Использует

Указатели функций позволяют передавать функции в качестве аргументов другим функциям или структурам данных. Таким образом, вы можете избежать использования временных переменных и вместо этого просто передать результат функции непосредственно следующей функции.

Указатели на функции также предотвращают повторение кода для разных типов данных. Например, обычно для создания программы быстрой сортировки для разных типов данных вам нужно написать функции сортировки для конкретных типов, такие как quicksort_integerили quicksort_char.

С помощью указателей функций вы могли бы вместо этого создать функцию, которая принимает указатели в качестве аргументов и сортирует их по любому полю, для которого у вас есть компаратор.

Наконец, указатели на функции позволяют создавать структуры, заполненные функциями, а не переменными. Например, вы можете создать массив функций, сохранив каждый указатель на функцию как элемент.

Используйте указатели на функции, когда вы хотите подключить одну функцию к другой или когда вы хотите ослабить ограничения типов при использовании структур данных.

Выполнение

# include <stdio.h>
void dbl ( int * ) ;
void tple ( int * ) ;
void qdpl ( int * ) ;
int main( )
{
    int  num = 2, i ;
    void ( *p[ ] )( int * ) = { dbl, tple, qdpl } ;
    for ( i = 0 ; i < 3 ; i++ )
    {
        p[ i ]( &num ) ;
        printf ( «%d\n», num ) ;
    }
    return 0 ;
}
void dbl ( int *n )
{
    *n = *n * *n ;
}
void tple ( int *n )
{
    *n = *n * *n * *n ;
}
void qdpl ( int *n )
{
    *n = *n * *n * *n * *n ;
}

В этой программе мы определили три функции. Функции dble, tpleи qdplполучают адрес типа int, удваивают / утраивают / учетверяют значение int, соответственно, и ничего не возвращают.

В основной функции мы сохраняем адреса этих функций в массиве, а затем вызываем их один за другим, выполняя итерацию по массиву.

В строке 8, мы создаем массив указателя функции и хранить адреса dble, tpleи qdplв массиве.

А также в строке 11 мы вызываем функции dbl, tpleи qdplпо их адресам путем итерации по массиву.

4. Рекурсия в C

Определение

Рекурсия — это когда функция вызывает саму себя. Рекурсивные программы часто содержат команды и операции над рекурсивным вызовом, которые повторяются на каждой рекурсивной итерации.

В C вы можете рекурсивно вызывать как пользовательские функции, так и main(). Рекурсия может заменить традиционные циклы во многих случаях. Как и циклы, рекурсивные программы могут повторяться бесконечно, если они запрограммированы без условия выхода.

Типичные рекурсивные вызовы происходят внутри ifсегмента или elseсегмента.

Для ifрекурсивных программ типа рекурсивный вызов удерживается в ifсегменте. Программа будет продолжать повторяться до тех пор, пока не ifстанет false. Затем программа перейдет к elseсегменту, содержащему оператор возврата.

Например:

int example()
{
  if (!= 0){ 
    // do something
    // something more
    example(..); //recursive call
  }
  else 
  {
    return 1;
  }
}

В программах elseтипа рекурсивный вызов удерживается в elseсегменте вместе с операциями, которые влияют на условия программы. elseПрограмма любого типа повторяется до тех пор, пока ifоператор не станет true.

Разница между этими типами программ, если мы прогрессирующими далеки от исходного состояния или по направлению к целевому состоянию.

Например:

int example ()
{
  if (== 0){
      return 1; 
  }
  else 
  {
    // do something
    // something more
    example(..); //recursive call
  }
}

Использует

Рекурсия используется для переориентации проблем, чтобы сосредоточиться на желаемом условии, а не на количестве итераций для его достижения.

Читайте также:  Представление переменной Int в памяти в C++

С циклами вы должны запрограммировать количество итераций, которые будет выполнять программа. Это означает, что вы должны знать, сколько итераций необходимо для данного ввода, или создать подкомпонент, который может определять количество итераций.

Однако вы в конечном итоге заинтересованы в достижении определенной цели, и итерации — ваш инструмент для этого, а не наоборот.

Рекурсия позволяет вам разрабатывать программы, которые ставят на передний план это целенаправленное мышление. Рекурсивные программы повторяются столько раз, сколько требуется для достижения своей цели, будь то отклонение от начального состояния или достижение целевого состояния.

Вы не должны использовать рекурсию для всего, потому что она использует больше ресурсов, чем решения итеративного цикла. В общем, рекурсия лучше всего подходит для решений, которые:

  • Можно решить, используя ответы на множество мелких подзадач.
  • Требовать повторения одних и тех же шагов несколько раз
  • Поддерживайте определенные состояния программы.

Выполнение

Эта программа на C находит факториал переданного целочисленного аргумента. Это else типовая рекурсивная программа.

#include <stdio.h>
int refact ( int ) ;
int main( )
{
    int  num, fact ;
    num = 4;
    fact = refact ( num ) ;
    printf ( «Factorial value = %d\n», fact ) ;
    return 0 ;
}
int  refact ( int  n )
{
    int  p ;
    if ( n == 0 )
        return ( 1 ) ;
    else
        p = n * refact ( n — 1 ) ; //recursive call
    return ( p ) ;
}

5. Приведение типов и определение типов в C

Определение

Приведение типов — это особый тип операции в C, передающей один тип данных другому. Приведение типов может выполняться неявно или явно.

Неявное приведение типов — это когда компилятор автоматически преобразует данные. Неявное приведение типов проще реализовать, но в процессе преобразования часто теряется информация.

// implicit typecasting
# include <stdio.h>
int main( )
{
    // Initialize variables of type int
    int  a = 5, b = 2, c ;
    // Declare variable of type int
    float  d ;
    // Assign value to c
    c = a / b ;
    printf ( «%d\n», c ) ;
    // Store result of integer division in variable of float type
    d = a / b ;
    printf ( «%f\n», d ) ;
}

Здесь dдолжно быть значение, 2.500000но это на самом деле 2.000000. Десятичное значение было усечено во время автоматического преобразования, так как оба aи bявляются целыми числами и поэтому возвращают целочисленное значение из своего деления.

Явное приведение типов решает эту проблему. С помощью этой формы преобразования типов вы вместо этого вручную конвертируете данные и, следовательно, можете выбрать более раннюю точку преобразования.

// explicit typecasting
# include <stdio.h>
int main( )
{
    // Initialize variables of type int
    int  a = 5, b = 2, c ;
    // Declare variable of type int
    float  d ;
    // Assign value to c
    c = a / b ;
    printf ( «%d\n», c ) ;
    // Store result of float division in variable of float type
    d = ( float ) a / b ;
    printf ( «%f\n», d ) ;
}

На линии 12, d = ( float ) a / bа является напечатанной силой в поплавок, прежде чем выполнять операцию деления. Следовательно, операция сохраняет все десятичные значения операции. Как видите, значение d- это 2.500000когда явно приведено.

Преобразуйте только менее конкретные типы данных в более конкретные типы данных, чтобы избежать потери данных. Например, вы сохраните всю информацию, если конвертируете из int→, doubleно вы потеряете информацию, если конвертируете из double→ int.

Использует

Вы можете использовать приведение типов для создания кода многократного использования. Если вы конвертируете меньшие данные в большие, вы можете использовать одни и те же значения в качестве аргументов для нескольких функций, даже если они вызывают разные типы данных.

Вы также можете взять вывод из первой последовательности функций и привести его к новому типу, чтобы соответствовать потребностям любой следующей функции, которую он выполняет. Приведение типов дает большую степень свободы при использовании строгих типов данных в C.

Вы также можете выполнить приведение типов, чтобы присвоить новые имена существующим типам данных или определяемым пользователем структурам с помощью typedefключевого слова. Это не отменяет присвоение исходного имени, а просто добавляет другой способ ссылки на тот же тип.

Например:

#include<stdio.h>

int main() {
    // Declare variables
    unsigned long int i, j;
    // Give new name to unsigned long int
    typedef unsigned long int ULI;
    // Declare variables of type ULI
    ULI k, l;
}

Здесь мы присваиваем имя ULIк unsigned long intтипу. Затем мы можем объявить переменные типа ULIили unsigned long intи добиться того же эффекта.

Это может улучшить читаемость, поскольку вы можете назначать имена типам данных, чтобы сократить объявления или более точно представлять, какие значения они будут содержать ( string→ color).

Выполнение

Неявное приведение типов:

// implicit typecasting
# include <stdio.h>
int main( )
{
    // Initialize variables of type int
    int  a = 5, b = 2, c ;
    // Declare variable of type int
    float  d ;
    // Assign value to c
    c = a / b ;
    printf ( «%d\n», c ) ;
    // Store result of integer division in variable of float type
    d = a / b ;
    printf ( «%f\n», d ) ;
}

Явное приведение типов:

# include <stdio.h>
int main( )
{
    // Initialize variables of type int
    int  a = 5, b = 2, c ;
    // Declare variable of type int
    float  d ;
    // Assign value to c
    c = a / b ;
    printf ( «%d\n», c ) ;
    // Store result of float division in variable of float type
    d = ( float ) a / b ;
    printf ( «%f\n», d ) ;
}

Typedef для пользовательской структуры:

#include<stdio.h>
int main() {
    // Declare structure
    struct a
    {
        char *p;
    };
    // Give new name to struct a
    typedef struct a FILE;
    FILE *fs , *ft;
}

Что изучать дальше

Поздравляем, вы сделали первые шаги в продвинутом программировании на C! Мы рассмотрели 5 наиболее важных концепций для улучшения ваших навыков программирования на C.

Однако это лишь малая часть того, что может предложить этот давно разработанный язык. Следующие хорошие темы для изучения:

  • Создание библиотек
  • C с Linux / Unix
  • Биты и побитовые операторы
  • Списки переменных аргументов
  • Структуры
Оцените статью
bestprogrammer.ru
Добавить комментарий