A variable that holds a variable number of values is called an array.
var=value
Is scalar variable assignment syntax, where one value is assigned to a scalar variable.
var=( one two 'three or 3' )
Is array variable assignment syntax¹, where you can assign any number (even 0²) of values to an array/list variable.
var[n]=foo
Is also scalar assignment syntax but assigns one value to the nth element³ of the array.
So, here you want:
extra_options=( -I ../dir1 -I ../dir2 -I '/opt/my software/include' )
gcc -M $extra_options somefile.c
Note that $extra_options expansion skips the empty elements of the $extra_options array if any, like $scalar expands to no argument rather than one empty element if $scalar is empty.
You need "$scalar" or "$array[@]" or "${(@)array}" (or "${array[@]}", compatible with ksh-like shells such as bash and reminiscent of the "$@" of the Bourne shell) to preserve the empty elements.
Note that in other Bourne-like shells, in:
TEMPP='-I ../dir1 -I ../dir2'
gcc -M $TEMPP somefile.c
$TEMPP undergoes an implicit so-called split+glob operator, it is not (thankfully!) as if the value of $TEMPP had been embedded as-is in the shell code like in gcc -M -I ../dir1 -I ../dir2 somefile.c.
If it were the case4, with:
TEMPP='foo>/etc/passwd;reboot' # or '$(reboot>/etc/passwd)'
echo $TEMPP
Would overwrite /etc/passwd and reboot for instance. Shell syntax including ;, > operators, ${...}, $(...) expansions, quotes is not recognised or handled upon variable expansions. It's however not expanded as-is like in zsh or other saner modern shells (like rc, es, fish, akanga...) or other programming languages, but undergoes that split+glob operator.
The value of $TEMPP is only split on characters of $IFS and then subject to globbing (aka filename generation or pathname expansion). What that means is that:
TEMPP='-I ../dir1 -I ../dir2'
gcc -M $TEMPP somefile.c
Only works if $IFS, at the point of interpreting the gcc... line happens to contain the space character and doesn't contain any of the other characters in $TEMPP (thankfully, that's the case with the default value of $IFS). For instance, if $IFS contained i instead, gcc would be called with -I ../d, r1 -I ../d and r2 arguments instead.
And:
TEMPP='-I ../dir1 -I ../dir2 -I "/opt/my software/include"'
gcc -M $TEMPP somefile.c
Wouldn't work, as we just have splitting on $IFS, not interpretation of shell syntax such as quoting, so if you had to pass arguments containing spaces, you'd need a different separator for the splitting such as:
TEMPP='-I,../dir1,-I,../dir2,-I,/opt/my software/include'
IFS=,
gcc -M $TEMPP somefile.c
For instance. And if you had values containing globbing operators such as *, ?, [...], you would need to issue a set -o noglob command before the expansion to make sure globs are not expanded. See also Security implications of forgetting to quote a variable in bash/POSIX shells for the security implications of that misdesign of Bourne-like shells (other than zsh).
In zsh, if you want $IFS-splitting and/or globbing (but you generally don't as zsh like most other modern shells has arrays), you have to request it explicitly, $=scalar to split on $IFS, $~scalar for the contents of $scalar to be considered as a pattern and subject to globbing or $=~scalar to have both and therefore the equivalent of $scalar in other Bourne-like shells.
See also ${(s[whatever])scalar} to split on arbitrary separators instead of $IFS, with ${(f)scalar} and ${(0)scalar} as shorthands to split on linefeed (aka newline) or NULs.
And if you need zsh to interpret code written for other Bourne-like or Korn-like shells, you'd use its sh or ksh emulations where compatibility with those shells is improved with:
emulate ksh: change the emulation definitely (though emulate zsh would restore the default saner mode.
emulate -L ksh: same but only Locally (in the current function for instance).
emulate ksh -c 'ksh/bash code here' or emulate ksh -c 'source some-ksh-or-bash-script' to interpret only the given code in that emulation mode.
In sh or ksh emulation, IFS-splitting and globbing are done implicitly upon unquoted parameter expansions in list contexts like in sh/ksh/bash (the shwordsplit and globsubst and many other options that affect the behaviour of the shell are tweaked to align with the behaviour of those other shells as closely as possible).
¹ inspired by the set var = ( one two 'three or 3' ) of csh, the first shell to introduce arrays and later copied by other shells like ksh93 (though earlier versions of ksh had set -A var one two 'three or 3' for that), bash or yash.
² note: in ksh93, var=() without declaring var as array first creates a compound variable rather than an array variable.
³ in zsh and all other shells but with one exception: ksh (and bash which copied ksh instead of zsh), where instead array indices start at 0 and arrays are sparse, so array[1]=x would assign the second element if the one with index 0 was also set, and array[123]=x would assign the first element if all those from 0 to 122 were not otherwise set.
4actually, that was the case in the first (very simple and primitive) Unix shell from the early 70s which didn't have variables nor command substitution but had $1...$9 positional parameters which expanded like that.